Spring Bean的配置及常用属性

一、Java Spring框架是什么?它有哪些好处?

Spring 是另一个主流的 Java Web 开发框架,该框架是一个轻量级的应用框架,具有很高的凝聚力和吸引力。Spring 框架因其强大的功能以及卓越的性能而受到众多开发人员的喜爱。

Spring 是分层的 Java SE/EE full-stack 轻量级开源框架,以 IoC(Inverse of Control,控制反转)和 AOP(Aspect Oriented Programming,面向切面编程)为内核,使用基本的 JavaBean 完成以前只可能由 EJB 完成的工作,取代了 EJB 臃肿和低效的开发模式。

在实际开发中,通常服务器端采用三层体系架构,分别为表现层(web)、业务逻辑层(service)、持久层(dao)。

Spring 对每一层都提供了技术支持,在表现层提供了与

Struts2

框架的整合,在业务逻辑层可以管理事务和记录日志等,在持久层可以整合

Hibernate

和 JdbcTemplate 等技术。

从设计上看,Spring 框架给予了 Java 程序员更高的自由度,对业界的常见问题也提供了良好的解决方案,因此,在开源社区受到了广泛的欢迎,并且被大部分公司作为 Java 项目开发的首选框架。

Spring 具有简单、可测试和松耦合等特点,不仅可以用于服务器端的开发,也可以应用于任何 Java 应用的开发中。Spring 框架的主要优点具体如下。

(1)方便解耦,简化开发

Spring 就是一个大工厂,可以将所有对象的创建和依赖关系的维护交给 Spring 管理。

(2)方便集成各种优秀框架

Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如 Struts2、Hibernate、MyBatis 等)的直接支持。

(3)降低 Java EE API 的使用难度

Spring 对 Java EE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用的难度大大降低。

(4)方便程序的测试

Spring 支持 JUnit4,可以通过注解方便地测试 Spring 程序。

(5)AOP 编程的支持

Spring 提供面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能。

(6)声明式事务的支持

只需要通过配置就可以完成对事务的管理,而无须手动编程。

二、Spring IoC容器:BeanFactory和ApplicationContext

在教程前面介绍

Spring

框架时,已经提到过 Spring 的 IoC(控制反转)思想,本节来详细介绍一下 Spring 的 Ioc 容器。

IoC 是指在程序开发中,实例的创建不再由调用者管理,而是由 Spring 容器创建。Spring 容器会负责控制程序之间的关系,而不是由程序代码直接控制,因此,控制权由程序代码转移到了 Spring 容器中,控制权发生了反转,这就是 Spring 的 IoC 思想。

Spring 提供了两种 IoC 容器,分别为 BeanFactory 和 ApplicationContext,接下来将针对这两种 IoC 容器进行详细讲解。

BeanFactory

BeanFactory 是基础类型的 IoC 容器,它由 org.springframework.beans.facytory.BeanFactory 接口定义,并提供了完整的 IoC 服务支持。简单来说,BeanFactory 就是一个管理 Bean 的工厂,它主要负责初始化各种 Bean,并调用它们的生命周期方法。

BeanFactory 接口有多个实现类,最常见的是 org.springframework.beans.factory.xml.XmlBeanFactory,它是根据 XML 配置文件中的定义装配 Bean 的。

创建 BeanFactory 实例时,需要提供 Spring 所管理容器的详细配置信息,这些信息通常采用 XML 文件形式管理。其加载配置信息的代码具体如下所示:

BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource(“D://applicationContext.xml”));

ApplicationContext

ApplicationContext 是 BeanFactory 的子接口,也被称为应用上下文。该接口的全路径为 org.springframework.context.ApplicationContext,它不仅提供了 BeanFactory 的所有功能,还添加了对 i18n(国际化)、资源访问、事件传播等方面的良好支持。

ApplicationContext 接口有两个常用的实现类,具体如下。

(1)ClassPathXmlApplicationContext

该类从类路径 ClassPath 中寻找指定的 XML 配置文件,找到并装载完成 ApplicationContext 的实例化工作,具体如下所示。

1
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(String configLocation);

在上述代码中,configLocation 参数用于指定 Spring 配置文件的名称和位置,如 applicationContext.xml。

(2)FileSystemXmlApplicationContext

该类从指定的文件系统路径中寻找指定的 XML 配置文件,找到并装载完成 ApplicationContext 的实例化工作,具体如下所示。

1
ApplicationContext applicationContext = new FileSystemXmlApplicationContext(String configLocation);

它与 ClassPathXmlApplicationContext 的区别是:在读取 Spring 的配置文件时,FileSystemXmlApplicationContext 不再从类路径中读取配置文件,而是通过参数指定配置文件的位置,它可以获取类路径之外的资源,如“F:/workspaces/applicationContext.xml”。

在使用 Spring 框架时,可以通过实例化其中任何一个类创建 Spring 的 ApplicationContext 容器。

通常在

Java

项目中,会采用通过 ClassPathXmlApplicationContext 类实例化 ApplicationContext 容器的方式,而在 Web 项目中,ApplicationContext 容器的实例化工作会交由 Web 服务器完成。Web 服务器实例化 ApplicationContext 容器通常使用基于 ContextLoaderListener 实现的方式,它只需要在 web.xml 中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--指定Spring配置文件的位置,有多个配置文件时,以逗号分隔-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--spring将加载spring目录下的applicationContext.xml文件-->
<param-value>
classpath:spring/applicationContext.xml
</param-value>
</context-param>
<!--指定以ContextLoaderListener方式启动Spring容器-->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

需要注意的是,BeanFactory 和 ApplicationContext 都是通过 XML 配置文件加载 Bean 的。

二者的主要区别在于,如果 Bean 的某一个属性没有注入,则使用 BeanFacotry 加载后,在第一次调用 getBean() 方法时会抛出异常,而 ApplicationContext 则在初始化时自检,这样有利于检查所依赖的属性是否注入。

因此,在实际开发中,通常都选择使用 ApplicationContext,而只有在系统资源较少时,才考虑使用 BeanFactory。本教程中使用的就是 ApplicationContext

三、第一个Spring程序

通过《Spring IoC容器》的学习,读者对 Spring 的 IoC 容器已经有了一个初步的了解。下面通过具体的案例演示 IoC 容器的使用。

1. 创建项目

在 MyEclipse 中创建 Web 项目 springDemo01,将 Spring 框架所需的 JAR 包复制到项目的 lib 目录中,并将添加到类路径下,添加后的项目如图 1 所示。

Spring所需的JAR包
图 1 Spring所需的JAR包

2. 创建 PersonDao 接口

在项目的 src 目录下创建一个名为 com.mengma.ioc 的包,然后在该包中创建一个名为 PersonDao 的接口,并在接口中添加一个 add() 方法,如下所示。

1
package com.mengma.ioc;public interface PersonDao {    public void add();}

3. 创建接口实现类 PersonDaoImpl

在 com.mengma.ioc 包下创建 PersonDao 的实现类 PersonDaoImpl,编辑后如下所示。

1
package com.mengma.ioc;public class PersonDaoImpl implements PersonDao {    @Override    public void add() {        System.out.println("save()执行了...");    }}

上述代码中,PersonDaoImpl 类实现了 PersonDao 接口中的 add() 方法,并且在方法调用时会执行输出语句。

4. 创建 Spring 配置文件

在 src 目录下创建 Spring 的核心配置文件 applicationContext.xml,编辑后如下所示。

1
<?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:p="http://www.springframework.org/schema/p"    xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">    <!-- 由 Spring容器创建该类的实例对象 -->    <bean id="personDao" class="com.mengma.ioc.PersonDaoImpl" /></beans>

上述代码中,第 2~5 行代码是 Spring 的约束配置,第 7 行代码表示在 Spring 容器中创建一个 id 为 personDao 的 bean 实例,其中 id 表示文件中的唯一标识符,class 属性表示指定需要实例化 Bean 的实全限定类名(包名+类名)。

需要注意的是,Spring 的配置文件名称是可以自定义的,通常情况下,都会将配置文件命名为 applicationContext.xml(或 bean.xml)。

5. 编写测试类

在 com.mengma.ioc 包下创建测试类 FirstTest,并在该类中添加一个名为 test1() 的方法,编辑后如下所示。

1
package com.mengma.ioc;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class FirstTest {    @Test    public void testl() {        // 定义Spring配置文件的路径        String xmlPath = "applicationContext.xml";        // 初始化Spring容器,加载配置文件        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(                xmlPath);        // 通过容器获取personDao实例        PersonDao personDao = (PersonDao) applicationContext                .getBean("personDao");        // 调用 personDao 的 add ()方法        personDao.add();    }}

上述代码中,首先定义了 Spring 配置文件的路径,然后创建 Spring 容器,接下来通过 Spring 容器获取了 personDao 实例,最后调用实例的 save() 方法。

6. 运行项目并查看结果

使用 JUnit 测试运行 test1() 方法,运行成功后,控制台的输出结果如图 2 所示。

从图 2 的输出结果中可以看出,程序已经成功输出了“save()执行了…”语句。在程序执行时,对象的创建并不是通过 new 一个类完成的,而是由 Spring 容器管理实现的。这就是 Spring IoC 容器思想的工作机制。

输出结果

四、Spring Bean的配置及常用属性

作为 Spring 核心机制的依赖注入,改变了传统的编程习惯,对组件的实例化不再由应用程序完成,转而交由 Spring 容器完成,在需要时注入应用程序中,从而对组件之间依赖关系进行了解耦。这一切都离不开 Spring 配置文件中使用的 元素。 Spring 容器可以被看作一个大工厂,而 Spring 容器中的 Bean 就相当于该工厂的产品。如果希望这个大工厂能够生产和管理 Bean,这时则需要告诉容器需要哪些 Bean,以及需要以何种方式将这些 Bean 装配到一起。 Spring 配置文件支持两种不同的格式,分别是 XML 文件格式和 Properties 文件格式。 通常情况下,Spring 会以 XML 文件格式作为 Spring 的配置文件,这种配置方式通过 XML 文件注册并管理 Bean 之间的依赖关系。 XML 格式配置文件的根元素是 ,该元素包含了多个 子元素,每一个 子元素定义了一个 Bean,并描述了该 Bean 如何被装配到 Spring 容器中。

定义 Bean 的示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<!-- 使用id属性定义person1,其对应的实现类为com.mengma.person1 -->
<bean id="person1" class="com.mengma.damain.Person1" />
<!--使用name属性定义person2,其对应的实现类为com.mengma.domain.Person2-->
<bean name="Person2" class="com.mengma.domain.Person2"/>
</beans>

在上述代码中,分别使用 id 和 name 属性定义了两个 Bean,并使用 class 元素指定了 Bean 对应的实现类。

元素中包含很多属性.

五、Spring CGLlB动态代理(附带实例)

CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类。因此 CGLIB 要依赖于 ASM 的包,解压 Spring 的核心包 spring-core-3.2.2.RELEASE.jar,文件目录如图 1 所示。

spring-core-3.2.2.RELEASE.jar文件

在图 1 中可以看出,解压的核心包中包含 cglib 和 asm,也就是说 Spring3.2.13 版本的核心包已经集成了 CGLIB 所需要的包,所以在开发中不需要另外导入 ASM 的 JAR 包了。下面通过案例演示实现 CGLIB 的代理过程。

1. 创建目标类 GoodsDao

在 com.mengma.dao 包下创建目标类 GoodsDao,在类中定义增、删、改、查方法,并在每个方法编写输出语句,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mengma.dao;
public class GoodsDao {
public void add() {
System.out.println("添加商品...");
}
public void update() {
System.out.println("修改商品...");
}
public void delete() {
System.out.println("删除商品...");
}
public void find() {
System.out.println("修改商品...");
}
}

2. 创建代理类 MyBeanFactory

在 src 目录下创建一个名为 com.mengma.cglib 的包,该包下创建类 MyBeanFactory,如下所示。

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
package com.mengma.cglib;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import com.mengma.dao.GoodsDao;
import com.mengma.jdk.MyAspect;
public class MyBeanFactory {
public static GoodsDao getBean() {
// 准备目标类
final GoodsDao goodsDao = new GoodsDao();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 生成代理类,CGLIB在运行时,生成指定对象的子类,增强
Enhancer enhancer = new Enhancer();
// 确定需要增强的类
enhancer.setSuperclass(goodsDao.getClass());
// 添加回调函数
enhancer.setCallback(new MethodInterceptor() {
// intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
myAspect.myBefore(); // 前增强
Object obj = method.invoke(goodsDao, args); // 目标方法执行
myAspect.myAfter(); // 后增强
return obj;
}
});
// 创建代理类
GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create();
return goodsDaoProxy;
}
}

上述代码中,应用了 CGLIB 的核心类 Enhancer。在第 19 行代码调用了 Enhancer 类的 setSuperclass() 方法,确定目标对象。

第 21 行代码调用 setCallback() 方法添加回调函数;第 24 行代码的 intercept() 方法相当于 JDK 动态代理方式中的 invoke() 方法,该方法会在目标方法执行的前后,对切面类中的方法进行增强;第 33~34 行代码调用 Enhancer 类的 create() 方法创建代理类,最后将代理类返回。

3. 创建测试类

在 com.mengma.cglib 包下创建测试类 CGLIBProxyTest,编辑后如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mengma.cglib;
import org.junit.Test;
import com.mengma.dao.GoodsDao;
public class CGLIBProxyTest {
@Test
public void test() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
GoodsDao goodsDao = MyBeanFactory.getBean();
// 执行方法
goodsDao.add();
goodsDao.update();
goodsDao.delete();
goodsDao.find();
}
}

上述代码中,调用 getBean() 方法时,依然获取的是 goodsDao 的代理对象,然后调用该对象的方法。使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图 2 所示。

输出结果
图 2 输出结果

从图 2 的输出结果中可以看出,在调用目标类的方法前后,也成功调用了增强的代码,由此说明,使用 CGLIB 代理的方式同样实现了手动代理。

六、Spring JDK动态代理(附带实例)

1. 创建项目

在 MyEclipse 中创建一个名称为 springDemo03 的 Web 项目,将

Spring

支持和依赖的 JAR 包复制到 Web 项目的 WEB-INF/lib 目录中,并发布到类路径下。

2. 创建接口 CustomerDao

在项目的 src 目录下创建一个名为 com.mengma.dao 的包,在该包下创建一个 CustomerDao 接口,编辑后如下所示。

1
2
3
4
5
6
7
package com.mengma.dao;
public interface CustomerDao {
public void add(); // 添加
public void update(); // 修改
public void delete(); // 删除
public void find(); // 查询
}

3. 创建实现类 CustomerDaoImpl

在 com.mengma.dao 包下创建 CustomerDao 接口的实现类 CustomerDaoImpl,并实现该接口中的所有方法,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.mengma.dao;
public class CustomerDaoImpl implements CustomerDao {
@Override
public void add() {
System.out.println("添加客户...");
}
@Override
public void update() {
System.out.println("修改客户...");
}
@Override
public void delete() {
System.out.println("删除客户...");
}
@Override
public void find() {
System.out.println("修改客户...");
}
}

4. 创建切面类 MyAspect

在 src 目录下,创建一个名为 com.mengma.jdk 的包,在该包下创建一个切面类 MyAspect,编辑后如下所示。

1
2
3
4
5
6
7
8
9
package com.mengma.jdk;
public class MyAspect {
public void myBefore() {
System.out.println("方法执行之前");
}
public void myAfter() {
System.out.println("方法执行之后");
}
}

上述代码中,在切面中定义了两个增强的方法,分别为 myBefore() 方法和 myAfter() 方法,用于对目标类(CustomerDaoImpl)进行增强。

5. 创建代理类 MyBeanFactory

在 com.mengma.jdk 包下创建一个名为 MyBeanFactory 的类,在该类中使用 java.lang.reflect.Proxy 实现 JDK 动态代理,如下所示。

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
package com.mengma.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.mengma.dao.CustomerDao;
import com.mengma.dao.CustomerDaoImpl;
public class MyBeanFactory {
public static CustomerDao getBean() {
// 准备目标类
final CustomerDao customerDao = new CustomerDaoImpl();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 使用代理类,进行增强
return (CustomerDao) Proxy.newProxyInstance(
MyBeanFactory.class.getClassLoader(),
new Class[] { CustomerDao.class }, new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
myAspect.myBefore(); // 前增强
Object obj = method.invoke(customerDao, args);
myAspect.myAfter(); // 后增强
return obj;
}
});
}
}

上述代码中,定义了一个静态的 getBean() 方法,这里模拟 Spring 框架的 IoC 思想,通过调用 getBean() 方法创建实例,第 14 行代码创建了 customerDao 实例。

第 16 行代码创建的切面类实例用于调用切面类中相应的方法;第 18~26 行就是使用代理类对创建的实例 customerDao 中的方法进行增强的代码,其中 Proxy 的 newProxyInstance() 方法的第一个参数是当前类的类加载器,第二参数是所创建实例的实现类的接口,第三个参数就是需要增强的方法。

在目标类方法执行的前后,分别执行切面类中的 myBefore() 方法和 myAfter() 方法。

6. 创建测试类 JDKProxyTest

在 com.mengma.jdk 包下创建一个名为 JDKProxyTest 的测试类,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.mengma.jdk;
import org.junit.Test;
import com.mengma.dao.CustomerDao;
public class JDKProxyTest {
@Test
public void test() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
CustomerDao customerDao = MyBeanFactory.getBean();
// 执行方法
customerDao.add();
customerDao.update();
customerDao.delete();
customerDao.find();
}
}

上述代码中,在调用 getBean() 方法时,获取的是 CustomerDao 类的代理对象,然后调用了该对象中的方法。

7. 运行项目并查看结果

使用 JUnit 测试运行 test() 方法,运行成功后,控制台的输出结果如图 1 所示。

从图 1 的输出结果中可以看出,在调用目标类的方法前后,成功调用了增强的代码,由此说明,JDK 动态代理已经实现。

运行结果

七、注解开发

1.0@autowired和@resource注解的区别是什么?

1.1区别:

​ 1、@Autowired注解由Spring提供,只按照byType注入;@resource注解由J2EE提供,默认按照byName自动注入。

​ 2、@Autowired默认按类型进行装配,@Resource默认按照名称进行装配。

  • 当注入容器存在多个同一类型的对象时,就是根据byName进行装配的
  • 当注入在IOC容器中该类型只有一个时,就通过byType进行装配

1.2解释:

@autowired

直接在属性上使用,也可以在set方法上使用!

使用Autowired可以不用写set方法了,前提是你这个自动装配的属性在IOC(Spring)容器中存在,且符合名字ByName!

科普:

1
@nullable  //字段标记了这个注解表示这个字段可以为null;
1
@Autowired(require = false)  表示这个对象可以为空,反之不可以为空
@Qualifier

限定哪个bean应该被自动注入。当Spring无法判断出哪个bean应该被注入时,@Qualifier注解有助于消除歧义bean的自动注入。

八、Spring自动装配Bean

除了使用 XML 和 Annotation 的方式装配 Bean 以外,还有一种常用的装配方式——自动装配。自动装配就是指 Spring 容器可以自动装配(autowire)相互协作的 Bean 之间的关联关系,将一个 Bean 注入其他 Bean 的 Property 中。

要使用自动装配,就需要配置 元素的 autowire 属性。autowire 属性有五个值,具体说明如表 1 所示。

名称 说明
byName 根据 Property 的 name 自动装配,如果一个 Bean 的 name 和另一个 Bean 中的 Property 的 name 相同,则自动装配这个 Bean 到 Property 中。
byType 根据 Property 的数据类型(Type)自动装配,如果一个 Bean 的数据类型兼容另一个 Bean 中 Property 的数据类型,则自动装配。
constructor 根据构造方法的参数的数据类型,进行 byType 模式的自动装配。
autodetect 如果发现默认的构造方法,则用 constructor 模式,否则用 byType 模式。
no 默认情况下,不使用自动装配,Bean 依赖必须通过 ref 元素定义。

下面通过修改《Spring基于Annotation装配Bean》中的案例演示如何实现自动装配。首先将applicationContext.xml 配置文件修改成自动装配形式,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="personDao" class="com.mengma.annotation.PersonDaoImpl" />
<bean id="personService" class="com.mengma.annotation.PersonServiceImpl"
autowire="byName" />
<bean id="personAction" class="com.mengma.annotation.PersonAction"
autowire="byName" />
</beans>

在上述配置文件中,用于配置 personService 和 personAction 的 元素中除了 id 和 class 属性以外,还增加了 autowire 属性,并将其属性值设置为 byName(按属性名称自动装配)。

默认情况下,配置文件中需要通过 ref 装配 Bean,但设置了 autowire=”byName”,Spring 会在配置文件中自动寻找与属性名字 personDao 相同的 ,找到后,通过调用 setPersonDao(PersonDao personDao)方法将 id 为 personDao 的 Bean 注入 id 为 personService 的 Bean 中,这时就不需要通过 ref 装配了。

使用 JUnit 再次运行测试类中的 test() 方法,控制台的显示结果如图 1 所示。

运行结果
图 1 运行结果

http://c.biancheng.net/view/4254.html

SpringBoot:快速入门(续)

SpringBoot:快速入门(续)

异步任务:

目录结构

1620352285813

helloController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.sec.controller;

import com.sec.service.helloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAsync//开启异步任务
public class helloController {

@Autowired
helloService hs;
@RequestMapping("/hello")
public String test(){
hs.hello();//停止三秒
return "ok";
}
}

helloService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sec.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class helloService {

@Async//告诉spring 这是一个异步方法
public void hello(){
try {
Thread.sleep(3000);//睡眠三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理中");
}
}

==注意:==

  • 首先要给方法前面写上注解@Async,告诉spring这是一个异步方法
  • 然后要在controller类前开启异步任务 @EnableAsync

邮件发送:

  1. 导入依赖:

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
  2. 编写代码

    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
    package com.sec;

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.mail.SimpleMailMessage;
    import org.springframework.mail.javamail.JavaMailSenderImpl;
    import org.springframework.mail.javamail.MimeMessageHelper;

    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    import java.io.File;
    import java.util.Random;

    @SpringBootTest
    class SpringBoot09AsyncApplicationTests {

    @Autowired
    JavaMailSenderImpl jsi;

    @Test
    //一个简单的邮件发送
    void contextLoads() {
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setSubject("郝嘉诚你好啊!");
    mailMessage.setText("感谢你的代码");
    mailMessage.setFrom("990784805@qq.com");
    mailMessage.setTo("990784805@qq.com");
    jsi.send(mailMessage);
    }

    @Test
    //一个复杂的邮件发送
    void contextLoads1() throws MessagingException {
    MimeMessage mimeMessage = jsi.createMimeMessage();
    //组装
    MimeMessageHelper messageHelper=new MimeMessageHelper(mimeMessage,true);
    //正文
    messageHelper.setSubject("尊敬的苏弋您好!");
    Random random = new Random();
    int num = random.nextInt(9000) + 1000;
    messageHelper.setText("<p style='color:red'>您的验证码是:"+num+"</p>",true);

    //附件
    messageHelper.addAttachment("1.jpg",new File("C:\\Users\\飞驰的苏弋\\Pictures\\Camera Roll\\1.jpg"));
    messageHelper.addAttachment("2.jpg",new File("C:\\Users\\飞驰的苏弋\\Pictures\\Camera Roll\\2.jpg"));

    //发送
    messageHelper.setFrom("990784805@qq.com");
    messageHelper.setTo("3046865110@qq.com");

    jsi.send(mimeMessage);

    }

    //把发送邮件的代码编写成了一个工具,可调用
    public void sendMail(Boolean html,String title,String text,Boolean textHtml,String sendFrom,String sendTo,String fileName,File file) throws MessagingException{
    MimeMessage mimeMessage = jsi.createMimeMessage();
    //组装
    MimeMessageHelper messageHelper=new MimeMessageHelper(mimeMessage,html);
    //正文
    messageHelper.setSubject(title);

    messageHelper.setText(text,textHtml);

    //附件
    messageHelper.addAttachment(fileName,file);

    //发送
    messageHelper.setFrom(sendFrom);
    messageHelper.setTo(sendTo);

    jsi.send(mimeMessage);
    }

    }

工具类调用法:

  • 发送邮件工具类:sendMail.java
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
package com.sec.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.util.Random;

@Service
public class sendMail {

@Autowired
JavaMailSenderImpl jsi;
public void sendMail(Boolean html, String title, String text, Boolean textHtml, String sendFrom, String sendTo, String fileName, File file) throws MessagingException {
MimeMessage mimeMessage = jsi.createMimeMessage();
//组装
MimeMessageHelper messageHelper=new MimeMessageHelper(mimeMessage,html);
//正文
messageHelper.setSubject(title);

messageHelper.setText(text,textHtml);

//附件
messageHelper.addAttachment(fileName,file);

//发送
messageHelper.setFrom(sendFrom);
messageHelper.setTo(sendTo);

jsi.send(mimeMessage);
}

public void hjc() throws MessagingException {
MimeMessage mimeMessage = jsi.createMimeMessage();
//组装
MimeMessageHelper messageHelper=new MimeMessageHelper(mimeMessage,true);
//正文
messageHelper.setSubject("尊敬的苏弋您好!");
Random random = new Random();
int num = random.nextInt(9000) + 1000;
messageHelper.setText("<p style='color:red'>您的验证码是:"+num+"</p>",true);

//附件
messageHelper.addAttachment("1.jpg",new File("C:\\Users\\飞驰的苏弋\\Pictures\\Camera Roll\\1.jpg"));
messageHelper.addAttachment("2.jpg",new File("C:\\Users\\飞驰的苏弋\\Pictures\\Camera Roll\\2.jpg"));

//发送
messageHelper.setFrom("990784805@qq.com");
messageHelper.setTo("3046865110@qq.com");

jsi.send(mimeMessage);
}
}

  • controller层调用:

    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
    package com.sec.controller;

    import com.sec.service.helloService;
    import com.sec.service.sendMail;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.mail.javamail.JavaMailSenderImpl;
    import org.springframework.mail.javamail.MimeMessageHelper;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;

    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    import javax.servlet.http.HttpSession;
    import java.io.File;

    @Controller
    @EnableAsync//开启异步任务
    public class helloController {

    @Autowired
    helloService hs;

    @Autowired
    sendMail sm;

    @RequestMapping({"/","/index","/index.html"})
    public String index(){
    return "/index";
    }


    @RequestMapping("/hello")
    @ResponseBody
    public String test(){
    hs.hello();//停止三秒
    return "ok";
    }

    @RequestMapping("/sendMail")
    public String sendMail(Model model) throws MessagingException {
    sm.sendMail(true,"飞驰的苏弋你好!","<h2 style='color:red'>你好呀飞驰的苏弋</h2>",true,"990784805@qq.com","3046865110@qq.com","5.png",new File("C:\\Users\\飞驰的苏弋\\Pictures\\Camera Roll\\5.png"));
    model.addAttribute("msg","发送成功");
    return "/index";
    }
    }

  • index.html页面编写一个按钮 结合Thymeleaf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <div>
    <p th:text="${msg}"></p>
    </div>
    <a th:href="@{/sendMail}">点击发送邮箱</a>
    </body>
    </html>

定时任务:

1
2
3
4
5
TaskScheduler	任务调度者
TaskExecutor 任务执行者

@EnableScheduling //开启定时功能的注解
@Scheduled //什么时候执行
  1. 开启定时功能

    1
    2
    3
    4
    5
    6
    7
    8
    @EnableAsync//开启异步任务
    @EnableScheduling//开启定时功能的注解
    @SpringBootApplication
    public class SpringBoot09AsyncApplication {
    public static void main(String[] args) {
    SpringApplication.run(SpringBoot09AsyncApplication.class, args);
    }
    }
  2. 编写定时任务的类
    cron表达式:是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义

    corn从左到右(用空格隔开):==秒 分 时 日 月 周几 年==

    ==注意事项:==

      每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

      (1)✳:表示匹配该域的任意值。假如在Minutes域使用✳, 即表示每分钟都会触发事件。

      (2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用,如果使用表示不管星期几都会触发,实际上并不是这样。

      (3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

      (4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.

      (5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

      (6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

      (7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。

      (8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

      (9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Service
    public class ScheduledService {
    //在一个特定的时间执行这个方法 Timer
    //cron表达式
    //秒 分 时 日 月 周几
    @Scheduled(cron = "59 7 19 * * ?")//每天的下午7点07分59秒执行
    public void hello(){
    System.out.println("hello,你被执行了");
    }
    }

RPC

  • 简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。
  • RPC 会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯)
  • RPC 是一个请求响应模型。客户端发起请求,服务器返回响应(类似于Http的工作方式)
  • RPC 在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。

1620352285813

分布式Dubbo + Zookeeper + SpringBoot

什么是dubbo?

Apache Dubbo 是一款高性能、轻量级的开源java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

dubbo的好处

  1. 透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
  2. 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。
  3. 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spring的Schema扩展进行加载。

dubbo基本概念

1620390645693

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

调用关系说明

服务容器负责启动,加载,运行服务提供者。

服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者在启动时,向注册中心订阅自己所需的服务。

注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

zookeeper

ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。
分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。

dubbo-admin安装测试

  1. 下载dubbo-admin
    地址:https://github.com/apache/dubbo-admin/tree/master

  2. 解压进入目录
    修改dubbo-admin\src\main\resource\application.properties 指定zookeeper地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server.port=7001
    spring.velocity.cache=false
    spring.velocity.charset=UTF-8
    spring.velocity.layout-url=/templates/default.vm
    spring.messages.fallback-to-system-locale=false
    spring.messages.basename=i18n/message
    spring.root.password=root
    spring.guest.password=guest

    dubbo.registry.address=zookeeper://127.0.0.1:2181
  3. 在项目目录下打包dubbo-admin

    1
    mvn clean package -Dmaven.test.skip=true

    第一次打包比较慢,耐心等待

  4. 执行dubbo-admin\target下的dubbo-admin-SNAPSHOT.jar

    1
    java -jar dubbo-admin-SNAPSHOT.jar

    【注意:zookeeper的服务一定要打开】
    执行完毕后,我们去访问http://localhost:7001/ ,这时候我们需要输入账户和密码,我们都是默认的root-root;
    登陆成功后,查看界面
    zookeeper:是一个注册中心
    dubbo-admin:是一个监控管理后台,查看我们注册了那些服务,消费了哪些服务
    Dubbo:jar包
    1620433176164

安装完成!
SpringBoot+Dubbo+zookeeper

服务注册发现实战

导入依赖

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
<!--导入依赖  Dubbo  zookeeper-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--日志会冲突-->
<!--引入zookeeper-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

框架搭建

  1. 启动zookeeper!

  2. IDEA创建一个空项目;

  3. 创建一个模块,实现服务提供者:provider-server,选择web依赖即可;

  4. 项目创建完毕,我们写一个服务,比如卖票的服务;

    编写接口

1
2
3
4
5
package com.sec.service;

public interface TicketService {
public String getTicket();
}

​ 编写实现类

provider-server提供的服务添加注解 ==这里@service不要导错包==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sec.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

@Service //可以被扫描到,在项目一启动就自动注册到注册中心
@Component //使用了dubbo后尽量不要用service注解
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "<苏弋>";
}
}

provider-server的==application.properties:==

1
2
3
4
5
6
7
8
server.port=8001

#服务应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务要被注册
dubbo.scan.base-packages=com.sec.service

再创建一个模块,实现消费者:consumer-server,选择web依赖;

编写类,再把TicketService接口拿到这边的service包下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.sec.service;

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service
public class UserService {
//想拿到provider-server提供的票 要去注册中心拿到服务
@Reference //引用 Pom坐标,可以定义路径相同的接口名
TicketService ticketService;
public String buyTicket(){
String ticket=ticketService.getTicket();
System.out.println("在注册中心拿到=>"+ticket);
return "在注册中心拿到=》"+ticket;
}
}

consumer-server的==application.properties==

1
2
3
4
5
6
server.port=8002

#消费去哪里拿服务需要暴露自己的名字
dubbo.application.name=consumer-server
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

controller调用buyTicket方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.sec.controller;

import com.sec.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class testController {

@Autowired
UserService userService;

@RequestMapping("/test")
@ResponseBody
public String test(){
String str = userService.buyTicket();
return str;
}
}

代码编写完毕;

测试运行

  1. 开启zookeeper服务,找到zkServer.cmd,双击运行。
    1620439926319
    1620439973214

  2. 到dubbo-admin-0.0.1-SNAPSHOT.jar的目录下,cmd运行 java -jar dubbo-admin-0.0.1-SNAPSHOT.jar。
    1620439994543
    1620440009406

  3. 开启提供者服务
    1620440033742

  4. 访问http://localhost:7001 查看提供者服务有没有
    1620440156252

  5. 访问http://localhost:8002/test
    1620440245065

  6. 查看后台Dubbo Admin的消费者是否产生

    1620440287811

  7. 完毕

总结步骤:

前提:zookeeper服务已开启

  1. 提供者提供服务
    1. 导入依赖
    2. 配置注册中心的地址,以及服务发现名,和要扫描的包
    3. 在想要被注册的服务上面增加一个注解==@Service== dubbo的注解
  2. 消费者如何消费
    1. 导入依赖
    2. 配置注册中心的地址,配置自己的服务名
    3. 从远程注入服务 @Reference

springboot实现websocket实时通信

springboot实现websocket实时通信

直接上代码

1.创建springboot项目

1691150993767

2.导入pom依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
</dependencies>

3.创建pojo类

Message.java

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
package com.hjc.mychat.pojo;

public class Message {
public String toName;
public String message;

@Override
public String toString() {
return "Message{" +
"toName='" + toName + '\'' +
", message='" + message + '\'' +
'}';
}

public String getToName() {
return toName;
}

public void setToName(String toName) {
this.toName = toName;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public Message(String toName, String message) {
this.toName = toName;
this.message = message;
}

public Message() {
}
}

Result.java

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
package com.hjc.mychat.pojo;

public class Result {
private boolean flag;
private String message;

public Result() {
}

public Result(boolean flag, String message) {
this.flag = flag;
this.message = message;
}

@Override
public String toString() {
return "Result{" +
"flag=" + flag +
", message='" + message + '\'' +
'}';
}

public boolean isFlag() {
return flag;
}

public void setFlag(boolean flag) {
this.flag = flag;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

ResultMessage.java

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
package com.hjc.mychat.pojo;

public class ResultMessage {
private boolean isSystemMessage;
private String fromName;
private Object message;

@Override
public String toString() {
return "ResultMessage{" +
"isSystemMessage=" + isSystemMessage +
", fromName='" + fromName + '\'' +
", message=" + message +
'}';
}

public boolean isSystemMessage() {
return isSystemMessage;
}

public void setSystemMessage(boolean systemMessage) {
isSystemMessage = systemMessage;
}

public String getFromName() {
return fromName;
}

public void setFromName(String fromName) {
this.fromName = fromName;
}

public Object getMessage() {
return message;
}

public void setMessage(Object message) {
this.message = message;
}

public ResultMessage(boolean isSystemMessage, String fromName, Object message) {
this.isSystemMessage = isSystemMessage;
this.fromName = fromName;
this.message = message;
}

public ResultMessage() {
}
}

4.utils工具类层

MessageUtils.java

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
package com.hjc.mychat.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hjc.mychat.pojo.ResultMessage;

public class MessageUtils {

public static String getMessage(boolean isSystemMessage,String fromName,Object message){
try {
ResultMessage result = new ResultMessage();
result.setSystemMessage(isSystemMessage);
result.setMessage(message);
if (fromName!=null){
result.setFromName(fromName);
}
//把字符串转成json格式的字符串
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
}catch (JsonProcessingException e){
e.printStackTrace();
}
return null;
}
}

5.controller层

pageController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.hjc.mychat.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class pageController {
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/main")
public String main(){
return "main";
}
}

userController.java

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
package com.hjc.mychat.controller;

import com.hjc.mychat.pojo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
public class userController {
@RequestMapping("/toLogin")
public Result tologin(@RequestParam("user") String user, @RequestParam("pwd") String pwd, HttpSession session){
Result result = new Result();
if (user.equals("张三")&&pwd.equals("123")){
result.setFlag(true);
session.setAttribute("user",user);
}else if (user.equals("李四")&&pwd.equals("123")){
result.setFlag(true);
session.setAttribute("user",user);
}else if (user.equals("123")&&pwd.equals("123")){
result.setFlag(true);
session.setAttribute("user",user);
}
else if (user.equals("王五")&&pwd.equals("123")){
result.setFlag(true);
session.setAttribute("user",user);
}else {
result.setFlag(false);
result.setMessage("登录失败");
}
return result;
}

@RequestMapping("/getUsername")
public String getUsername(HttpSession session){
String username = (String) session.getAttribute("user");
return username;
}

}


6.config层

WebsocketConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.hjc.mychat.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebsocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

7.websocket层

ChatEndpoint.java

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
package com.hjc.mychat.ws;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hjc.mychat.pojo.Message;
import com.hjc.mychat.utils.MessageUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint( value = "/hjcchat",configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {


//用来存储每个用户客户端对象的ChatEndpoint对象
private static Map<String,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

//声明session对象,通过对象可以发送消息给指定的用户
private Session session;

//声明HttpSession对象,我们之前在HttpSession对象中存储了用户名
private HttpSession httpSession;

//连接建立
@OnOpen
public void onOpen(Session session, EndpointConfig config){
this.session = session;
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
//存储登陆的对象
String username = (String)httpSession.getAttribute("user");
onlineUsers.put(username,this);

//将当前在线用户的用户名推送给所有的客户端
//1 获取消息
String message = MessageUtils.getMessage(true, null, getNames());
//2 调用方法进行系统消息的推送
broadcastAllUsers(message);
}

private void broadcastAllUsers(String message){
try {
//将消息推送给所有的客户端
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
chatEndpoint.session.getBasicRemote().sendText(message);
}
}catch (Exception e){
e.printStackTrace();
}
}

//返回在线用户名
private Set<String> getNames(){
return onlineUsers.keySet();
}

//收到消息
@OnMessage
public void onMessage(String message,Session session){
//将数据转换成对象
try {
ObjectMapper mapper =new ObjectMapper();
Message mess = mapper.readValue(message, Message.class);
String toName = mess.getToName();
String data = mess.getMessage();
String username = (String) httpSession.getAttribute("user");
String resultMessage = MessageUtils.getMessage(false, username, data);
//发送数据
onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
} catch (Exception e) {
e.printStackTrace();
}

}
//关闭
@OnClose
public void onClose(Session session) {
String username = (String) httpSession.getAttribute("user");
//从容器中删除指定的用户
onlineUsers.remove(username);
String message = MessageUtils.getMessage(true, null, getNames());
broadcastAllUsers(message);
}
}

GetHttpSessionConfigurator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.hjc.mychat.ws;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;


public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//获取HttpSession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}


8.前端页面

login.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>网络聊天室-登录</title>
<!--核心样式-->
<link rel="stylesheet" href="css/style.css">
</head>
<body>

<div class="container">
<div class="card"></div>
<div class="card">
<h1 class="title">用户登录</h1>
<form id="loginForm" method="post">
<div class="input-container">
<input type="text" id="user" name="user" required="required" />
<label for="user">用户名</label>
<div class="bar"></div>
</div>
<div class="input-container">
<input type="password" id="pwd" name="pwd" required="required" />
<label for="pwd">密码</label>
<div class="bar"></div>
</div>
<div class="button-container">
<button type="button" id="btn"><span>登录</span></button>
</div>
<div><p style="color: red;text-align: center" id="err_msg"></p></div>
</form>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$("#btn").click(function () {
$.get("toLogin?",$("#loginForm").serialize(),function(res){
if (res.flag){
console.log(res);
location.href = "main";
} else {
console.log(res);
$("#err_msg").html(res.message);
}
},"json");
})
$("#pwd").keypress(function (e) {
if(e.keyCode==13){
$.get("toLogin?",$("#loginForm").serialize(),function(res){
if (res.flag){
console.log(res);
location.href = "main";
} else {
console.log(res);
$("#err_msg").html(res.message);
}
},"json");
}

})
</script>
</body>
</html>

main.html

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
139
140
141
142
143
144
145
146
147
148
149
150
151
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网络聊天室</title>
<link rel="stylesheet" href="css/mycss.css">
</head>

<body>
<div id = "contains">
<div id="username"><h3 style="text-align: center;">用户:张三<span>-在线</span></h3></div>
<div id="Inchat"></div>
<div id="left">
<div id="content">

</div>
<div id="input">
<textarea type="text" id="input_text" style="width: 695px;height: 200px;"></textarea>
<button id="submit" style="float: right;">发送</button>
</div>
</div>
<div id="right">
<p id="hy" style="text-align: center;">好友列表</p>
<div id="hyList">

</div>
<p id="xt" style="text-align: center">系统消息</p>
<div id="xtList">

</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
var toName;
var username;
//点击好友名称展示相关消息
function showChat(name){
toName = name;
//现在聊天框
$("#content").html("");
$("#content").css("visibility","visible");
$("#Inchat").html("当前正与"+toName+"聊天");
//从sessionStorage中获取历史信息
var chatData = sessionStorage.getItem(toName);
if (chatData != null){
$("#content").html(chatData);
}
}
$(function () {
$.ajax({
url:"getUsername",
success:function (res) {
username = res;
},
async:false //同步请求,只有上面好了才会接着下面
});
//建立websocket连接
//获取host解决后端获取httpsession的空指针异常
var host = window.location.host;
var ws = new WebSocket("ws://"+host+"/hjcchat");
ws.onopen = function (evt) {
$("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-在线</span></h3>");
}
//接受消息
ws.onmessage = function (evt) {
//获取服务端推送的消息
var dataStr = evt.data;
//将dataStr转换为json对象
var res = JSON.parse(dataStr);

console.log(res)
//判断是否是系统消息
if(res.systemMessage){
//系统消息
//1.好友列表展示
//2.系统广播的展示
//此处声明的变量是调试时命名的,可以直接合并
var names = res.message;
var userlistStr = "";
var broadcastListStr = "";
var temp01 = "<a style=\"text-align: center; display: block;\" onclick='showChat(\"";
var temp03 = "\")'>";
var temp04 = "</a></br>";
var temp = "";
for (var name of names){
if (name != username){
temp = temp01 + name + temp03 + name + temp04;
userlistStr = userlistStr + temp;
broadcastListStr += "<p style='text-align: center'>"+ name +"上线了</p>";
}
}
//渲染好友列表和系统广播
$("#hyList").html(userlistStr);
$("#xtList").html(broadcastListStr);

}else {
//不是系统消息
var str = "<span id='mes_left'>"+ res.message +"</span></br>";
if (toName === res.fromName) {
$("#content").append(str);
}
var chatData = sessionStorage.getItem(res.fromName);
if (chatData != null){
str = chatData + str;
}
//保存聊天消息
sessionStorage.setItem(res.fromName,str);
};
}
ws.onclose = function () {
$("#username").html("<h3 style=\"text-align: center;\">用户:"+ username +"<span>-离线</span></h3>");
}

//发送消息
$("#submit").click(function () {
sendmessage();
});

//监听回车发送
$("#input_text").keypress(function (e) {
if(e.keyCode==13){
sendmessage();
}
});

function sendmessage() {
//1.获取输入的内容
var data = $("#input_text").val();
//2.清空发送框
$("#input_text").val("");
var json = {"toName": toName ,"message": data};
//将数据展示在聊天区
var str = "<span id='mes_right'>"+ data +"</span></br>";
$("#content").append(str);

var chatData = sessionStorage.getItem(toName);
if (chatData != null){
str = chatData + str;
}
sessionStorage.setItem(toName,str);
//3.发送数据
ws.send(JSON.stringify(json));
}
})
</script>
</body>

</html>

9.css样式

mycss.css

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
#contains{
background-color: pink;
width: 1000px;
height: 700px;
margin: auto;
}
#username{
background-color: powderblue;
width: 1000px;
height: 30px;
}
#Inchat{
background-color: rgb(5, 130, 146);
width: 1000px;
height: 30px;
}
#left{
background-color: salmon;
width: 700px;
height: 640px;
float: left;
position: relative;
}
#content{
background-color: silver;
width: 700px;
height: 400px;
/*display: none;*/
visibility: hidden;
}
#right{
background-color: rgb(107, 3, 3);
width: 300px;
height: 640px;
float: right;
}
#hyList{
height: 270px;
overflow-y: scroll;
background: antiquewhite;
}
#xtList{
height: 270px;
overflow-y: scroll;
background: antiquewhite;
}
#input{
margin-bottom: 0px;
position: absolute;
bottom: 0px;
}
#mes_left{
float: left;
border: 2px aqua;
max-width: 490px;
}
#mes_right{
float: right;
border: 2px aqua;
max-width: 490px;
}
#hy{
display: block;
width: 300px;
}
textarea {
resize: none;
border: none;
outline: none
}

style.css

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
body {
background: #e9e9e9;
color: deeppink;
font-family: "RobotoDraft", "Roboto", sans-serif;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

/* Pen Title */
.pen-title {
padding: 50px 0;
text-align: center;
letter-spacing: 2px;
}
.pen-title h1 {
margin: 0 0 20px;
font-size: 48px;
font-weight: 300;
}
.pen-title span {
font-size: 12px;
}
.pen-title span .fa {
color: #006a71;
}
.pen-title span a {
color: #006a71;
font-weight: 600;
text-decoration: none;
}

/* Container */
.container {
position: relative;
max-width: 460px;
width: 100%;
margin: 0 auto 100px;
}
.container.active .card:first-child {
background: #f2f2f2;
margin: 0 15px;
}
.container.active .card:nth-child(2) {
background: #fafafa;
margin: 0 10px;
}
.container.active .card.alt {
top: 20px;
right: 0;
width: 100%;
min-width: 100%;
height: auto;
border-radius: 5px;
padding: 60px 0 40px;
overflow: hidden;
}
.container.active .card.alt .toggle {
position: absolute;
top: 40px;
right: -70px;
box-shadow: none;
-webkit-transform: scale(10);
-moz-transform: scale(10);
-ms-transform: scale(10);
-o-transform: scale(10);
transform: scale(10);
transition: transform 0.3s ease;
}
.container.active .card.alt .toggle:before {
content: "";
}
.container.active .card.alt .title,
.container.active .card.alt .input-container,
.container.active .card.alt .button-container {
left: 0;
opacity: 1;
visibility: visible;
transition: 0.3s ease;
}
.container.active .card.alt .title {
transition-delay: 0.3s;
}
.container.active .card.alt .input-container {
transition-delay: 0.4s;
}
.container.active .card.alt .input-container:nth-child(2) {
transition-delay: 0.5s;
}
.container.active .card.alt .input-container:nth-child(3) {
transition-delay: 0.6s;
}
.container.active .card.alt .button-container {
transition-delay: 0.7s;
}

/* Card */
.card {
position: relative;
background: #ffffff;
border-radius: 5px;
padding: 60px 0 40px 0;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: 0.3s ease;
/* Title */
/* Inputs */
/* Button */
/* Alt Card */
}
.card:first-child {
background: #fafafa;
height: 10px;
border-radius: 5px 5px 0 0;
margin: 0 10px;
padding: 0;
}
.card .title {
position: relative;
z-index: 1;
border-left: 5px solid #006a71;
margin: 0 0 35px;
padding: 10px 0 10px 50px;
color: #006a71;
font-size: 32px;
font-weight: 600;
text-transform: uppercase;
}
.card .input-container {
position: relative;
margin: 0 60px 50px;
}
.card .input-container input {
outline: none;
z-index: 1;
position: relative;
background: none;
width: 100%;
height: 60px;
border: 0;
color: #212121;
font-size: 24px;
font-weight: 400;
}
.card .input-container input:focus ~ label {
color: #9d9d9d;
transform: translate(-12%, -50%) scale(0.75);
}
.card .input-container input:focus ~ .bar:before, .card .input-container input:focus ~ .bar:after {
width: 50%;
}
.card .input-container input:valid ~ label {
color: #9d9d9d;
transform: translate(-12%, -50%) scale(0.75);
}
.card .input-container label {
position: absolute;
top: 0;
left: 0;
color: #757575;
font-size: 24px;
font-weight: 300;
line-height: 60px;
-webkit-transition: 0.2s ease;
-moz-transition: 0.2s ease;
transition: 0.2s ease;
}
.card .input-container .bar {
position: absolute;
left: 0;
bottom: 0;
background: #757575;
width: 100%;
height: 1px;
}
.card .input-container .bar:before, .card .input-container .bar:after {
content: "";
position: absolute;
background: #ff7e67;
width: 0;
height: 2px;
transition: 0.2s ease;
}
.card .input-container .bar:before {
left: 50%;
}
.card .input-container .bar:after {
right: 50%;
}
.card .button-container {
margin: 0 60px;
text-align: center;
}
.card .button-container button {
outline: 0;
cursor: pointer;
position: relative;
display: inline-block;
background: 0;
width: 240px;
border: 2px solid #e3e3e3;
padding: 20px 0;
font-size: 24px;
font-weight: 600;
line-height: 1;
text-transform: uppercase;
overflow: hidden;
transition: 0.3s ease;
}
.card .button-container button span {
position: relative;
z-index: 1;
color: #ddd;
transition: 0.3s ease;
}
.card .button-container button:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: block;
background: #006a71;
width: 30px;
height: 30px;
border-radius: 100%;
margin: -15px 0 0 -15px;
opacity: 0;
transition: 0.3s ease;
}
.card .button-container button:hover, .card .button-container button:active, .card .button-container button:focus {
border-color: #68b0ab;
}
.card .button-container button:hover span, .card .button-container button:active span, .card .button-container button:focus span {
color: #68b0ab;
}
.card .button-container button:active span, .card .button-container button:focus span {
color: #68b0ab;
}
.card .button-container button:active:before, .card .button-container button:focus:before {
opacity: 1;
-webkit-transform: scale(10);
-moz-transform: scale(10);
-ms-transform: scale(10);
-o-transform: scale(10);
transform: scale(10);
}
.card.alt {
position: absolute;
top: 40px;
right: -70px;
z-index: 10;
width: 140px;
height: 140px;
background: none;
border-radius: 100%;
box-shadow: none;
padding: 0;
transition: 0.3s ease;
/* Toggle */
/* Title */
/* Input */
/* Button */
}
.card.alt .toggle {
position: relative;
background: #006a71;
width: 140px;
height: 140px;
border-radius: 100%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
color: #ffffff;
font-size: 58px;
line-height: 140px;
text-align: center;
cursor: pointer;
}
.card.alt .toggle:before {
content: "\f040";
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform: translate(0, 0);
}
.card.alt .title,
.card.alt .input-container,
.card.alt .button-container {
left: 100px;
opacity: 0;
visibility: hidden;
}
.card.alt .title {
position: relative;
border-color: #ffffff;
color: #ffffff;
}
.card.alt .title .close {
cursor: pointer;
position: absolute;
top: 0;
right: 60px;
display: inline;
color: #ffffff;
font-size: 58px;
font-weight: 400;
}
.card.alt .title .close:before {
content: "\00d7";
}
.card.alt .input-container input {
color: #ffffff;
}
.card.alt .input-container input:focus ~ label {
color: #ffffff;
}
.card.alt .input-container input:focus ~ .bar:before, .card.alt .input-container input:focus ~ .bar:after {
background: #ff7e67;
}
.card.alt .input-container input:valid ~ label {
color: #ffffff;
}
.card.alt .input-container label {
color: rgba(255, 255, 255, 0.8);
}
.card.alt .input-container .bar {
background: rgba(255, 255, 255, 0.8);
}
.card.alt .button-container button {
width: 100%;
background: #ffffff;
border-color: #ffffff;
}
.card.alt .button-container button span {
color: #006a71;
}
.card.alt .button-container button:hover {
background: rgba(255, 255, 255, 0.9);
}
.card.alt .button-container button:active:before, .card.alt .button-container button:focus:before {
display: none;
}

/* Keyframes */
@-webkit-keyframes buttonFadeInUp {
0% {
bottom: 30px;
opacity: 0;
}
}
@-moz-keyframes buttonFadeInUp {
0% {
bottom: 30px;
opacity: 0;
}
}
@keyframes buttonFadeInUp {
0% {
bottom: 30px;
opacity: 0;
}
}

web前端开发技术考前复习

web前端开发技术考前复习

B/S和C/S的区别

B/S(Browser/Server)和C/S(Client/Server)是两种常见的应用程序架构模式。

B/S指基于浏览器和服务器的应用程序,其中所有的处理都在服务器上完成,用户通过浏览器作为客户端来访问应用程序。这种架构模式的好处是用户不需要安装任何客户端软件,只需要通过浏览器就可以访问应用程序,从而降低了维护成本和更新难度。

C/S指基于客户端和服务器的应用程序,其中客户端和服务器之间通过网络连接进行通信。在这种模式下,客户端负责一部分应用程序的逻辑处理,而服务器则负责另一部分逻辑处理。C/S应用程序通常需要在客户端上安装专门的客户端软件,因此对维护和更新的要求较高。

总体而言,B/S架构更加灵活,易于维护和升级,而C/S架构则可以提供更高的性能和更丰富的用户体验。具体选择哪种架构模式要根据实际需求和业务情况进行综合考虑。

H5和C3的新特性

我认为您指的是HTML5(或简写为H5)和CSS3(或简写为C3),它们是Web开发中最常用的技术之一。以下是它们的一些新特性:

HTML5:

  1. 语义化标签,如<header>,<footer>, <article>等。
  2. 新增表单元素,如日期选择器、搜索框、邮件地址输入框等。
  3. 视频和音频支持,无需插件即可播放媒体文件。
  4. 本地存储,可以在客户端保存数据。

CSS3:

  1. 强大的2D/3D变换和动画效果,如旋转、平移、缩放、倾斜等。
  2. 多列布局和弹性盒子布局,使得页面布局更加灵活。
  3. 自定义字体、渐变和阴影等效果。
  4. 媒体查询,根据设备屏幕大小调整CSS样式。

需要注意的是,这只是HTML5和CSS3中的一些新特性,并不是全部。同时,这些技术正在不断更新和完善,因此还有很多其他特性和功能。

JS事件处理有几种方式

在JavaScript中,事件处理有三种主要的方式:

  1. HTML事件处理:通过在HTML元素上使用内联事件处理器来指定处理函数。
  2. DOM级别事件处理:使用JavaScript代码将事件处理程序附加到DOM元素上,可以使用addEventListener()方法来添加事件监听器,或者使用on*属性(如onclick)将事件处理程序分配给元素。
  3. 对象级别事件处理:在JavaScript中创建自定义对象时,可以使用事件模型为该对象定义自定义事件并指定相应的事件处理程序。可以使用EventTarget接口中定义的addEventListener()、removeEventListener()和dispatchEvent()方法来处理这些自定义事件。

DOM和BOM的区别

DOM(文档对象模型)和BOM(浏览器对象模型)都是JavaScript与Web交互的重要部分,但它们有不同的作用:

  1. DOM描述了网页内容(如HTML、XML等),并定义了一种访问和操作这些内容的方法和接口。DOM的核心是一棵树形结构,由节点和对象组成,它表示了网页中所有的元素和标签以及它们之间的关系。

  2. BOM则提供了与浏览器窗口交互的对象和方法,例如window对象、location对象、history对象、navigator对象、screen对象等。BOM使得JavaScript能够控制浏览器的行为,例如打开新窗口、改变URL、导航历史记录等。

因此,DOM和BOM是JavaScript语言的两个最重要的API,而它们的主要区别在于它们所处理的对象和任务的不同。

JS消息对话框

JavaScript中的消息对话框是一种弹出式窗口,用于向用户显示一条消息并等待用户响应。它包括以下三种类型:

  • alert():用于向用户显示一条消息,并要求用户点击“确定”按钮来关闭对话框。
  • prompt():用于向用户显示一条消息和一个文本输入字段,并等待用户在该字段中输入文本以及点击“确定”或“取消”按钮。
  • confirm():用于向用户显示一条消息和两个按钮:“确定”和“取消”,并等待用户选择其中一个按钮。

CSS选择器有哪些,写出名称

CSS选择器用于选择要应用样式的元素。以下是一些常见的CSS选择器名称:

  • 标签选择器(tag selector)
  • 类选择器(class selector)
  • ID选择器(id selector)
  • 子元素选择器(child selector)
  • 后代选择器(descendant selector)
  • 相邻兄弟选择器(adjacent sibling selector)
  • 通用选择器(universal selector)
  • 属性选择器(attribute selector)
  • 伪类选择器(pseudo-class selector)
  • 伪元素选择器(pseudo-element selector)

还有一些其他的选择器,如组合选择器、否定伪类选择器等。

String字符串

String 对象方法

方法 描述
charAt() 返回在指定位置的字符。
charCodeAt() 返回在指定的位置的字符的 Unicode 编码。
concat() 连接两个或更多字符串,并返回新的字符串。
endsWith() 判断当前字符串是否是以指定的子字符串结尾的(区分大小写)。
fromCharCode() 将 Unicode 编码转为字符。
indexOf() 返回某个指定的字符串值在字符串中首次出现的位置。
includes() 查找字符串中是否包含指定的子字符串。
lastIndexOf() 从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置。
match() 查找找到一个或多个正则表达式的匹配。
repeat() 复制字符串指定次数,并将它们连接在一起返回。
replace() 在字符串中查找匹配的子串,并替换与正则表达式匹配的子串。
replaceAll() 在字符串中查找匹配的子串,并替换与正则表达式匹配的所有子串。
search() 查找与正则表达式相匹配的值。
slice() 提取字符串的片断,并在新的字符串中返回被提取的部分。
split() 把字符串分割为字符串数组。
startsWith() 查看字符串是否以指定的子字符串开头。
substr() 从起始索引号提取字符串中指定数目的字符。
substring() 提取字符串中两个指定的索引号之间的字符。
toLowerCase() 把字符串转换为小写。
toUpperCase() 把字符串转换为大写。
trim() 去除字符串两边的空白。
toLocaleLowerCase() 根据本地主机的语言环境把字符串转换为小写。
toLocaleUpperCase() 根据本地主机的语言环境把字符串转换为大写。
valueOf() 返回某个字符串对象的原始值。
toString() 返回一个字符串。

简述web工作原理

Web 的工作原理涉及多个组件和协议,包括客户端浏览器、服务器、HTTP 协议以及 HTML/CSS/JavaScript 等技术。

  1. 客户端浏览器向服务器发送 HTTP 请求,请求包括所需资源的 URL 和其他相关信息。
  2. 服务器接收到请求后,根据 URL 找到对应的资源并生成 HTTP 响应,响应包括状态码、消息头和消息体。
  3. 响应被传输回客户端浏览器,浏览器解析响应并渲染页面。如果响应是 HTML 页面,则浏览器会解析 HTML 标记,并使用 CSS 样式和 JavaScript 脚本来渲染和交互。
  4. 在页面渲染期间,浏览器可能会发起其他 HTTP 请求获取页面中引用的其他资源,如图片、样式表、JavaScript 文件等。
  5. 服务器再次响应这些请求,浏览器将这些资源嵌入到页面中或者执行对应的代码。

以上是 Web 工作原理的简要流程,其中还有许多细节和复杂性,包括缓存、Cookie、会话管理、HTTPS 等。

js常用函数

JavaScript中常用的函数包括:

  1. console.log():将消息打印到浏览器控制台。
  2. alert():在浏览器中弹出一个警告框,显示指定消息。
  3. prompt():在浏览器中弹出一个对话框,提示用户输入值,并返回该值。
  4. parseInt()parseFloat():将字符串转换为整数或浮点数。
  5. String():将变量转换为字符串。
  6. Array():创建一个新数组。
  7. push()pop():向数组末尾添加元素或从末尾删除元素。
  8. shift()unshift():从数组开头删除或添加元素。
  9. splice():从数组中删除、替换或插入元素。
  10. slice():从数组中提取一部分元素,返回一个新数组。
  11. indexOf()lastIndexOf():查找数组中特定元素的位置。
  12. join()toString():将数组转换为字符串。
  13. Math.random():生成一个0到1之间的随机数。
  14. Math.floor()Math.ceil():向下和向上取整。
  15. setInterval():每隔指定时间就执行一次函数。
  16. setTimeout():在指定时间后执行一次函数。
  17. typeof():返回数据类型。
  18. confirm():弹出一个确认对话框,让用户选择“确定”或“取消”。
  19. isNaN():检查给定的值是否是数字。

js中String字符串常用的方法

以下是 JavaScript 中 String 字符串常用的方法:

  1. length:获取字符串长度。
  2. charAt():返回指定索引位置的字符。
  3. concat():连接两个或多个字符串。
  4. indexOf():返回某个指定的字符串值在字符串中首次出现的位置。
  5. lastIndexOf():返回某个指定的字符串值在字符串中最后出现的位置。
  6. slice():提取字符串的一部分,并返回一个新的字符串。
  7. substring():提取字符串的一部分,并返回一个新的字符串。
  8. substr():从起始索引号提取字符串中指定数目的字符。
  9. replace():在字符串中用一些字符替换另一些字符。
  10. toUpperCase():将字符串转换为大写字母。
  11. toLowerCase():将字符串转换为小写字母。
  12. trim():去掉字符串两端的空格。
  13. split():将字符串分割成子字符串,并将结果存储到数组中。
  14. match():在字符串中搜索一个与正则表达式匹配的值。

js特点

以下是JavaScript的一些特点:

  1. 基于对象:JavaScript是一种基于对象的语言,它使用对象作为其编程模型的核心。
  2. 弱类型:JavaScript是一种弱类型语言,变量可以自动转换为所需的类型。
  3. 解释型:JavaScript是一种解释型语言,代码在运行时被逐行解析和执行。
  4. 跨平台:JavaScript可以在多种不同的环境中运行,包括浏览器、服务器和移动设备等。
  5. 事件驱动:JavaScript中的交互性主要是通过事件来实现的,例如鼠标点击、键盘输入等。
  6. 函数式编程:JavaScript中函数是一等公民,支持函数式编程范式。
  7. 动态性:JavaScript的动态性使得它能够在运行时修改程序的结构和行为。
  8. 简单易用:JavaScript相对于其他语言而言,学习曲线较低,易上手。

CSS动画animation

​ CSS3动画(animation)是一种利用CSS3技术实现的动画效果,通过定义关键帧(keyframes)和动画属性(animation properties)来描述元素的动画过程。它可以实现多种复杂的动画效果,如渐变、旋转、缩放、移动、透明度等,并且可以控制动画的速度、延迟、重复次数等。CSS3动画可以使网页更加生动有趣,提高用户体验。

以下是一个简单的CSS3动画(animation)的用法示例,具体实现效果可以在浏览器中查看:

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
<!doctype html>
<html>
<head>
<style>
/* 定义动画关键帧 */
@keyframes example {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}

/* 应用动画属性 */
.box {
width: 100px;
height: 100px;
background-color: red;
animation-name: example;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
</style>
</head>
<body>
<!-- 创建一个需要应用动画的元素 -->
<div class="box"></div>
</body>
</html>

这个示例中,我们创建了一个红色的正方形元素(box),并定义了一个名为example的动画关键帧,该动画会让元素以线性方式不断旋转360度。然后通过将animation-name、animation-duration、animation-timing-function和animation-iteration-count属性应用到.box元素上,来控制动画的名称、时长、速度曲线和重复次数。最终呈现出来的效果就是一个不停旋转的红色正方形。

js变量、常量及其用法

在JavaScript中,变量和常量都用于存储数据值。变量是可更改的数据存储选项,而常量则是不可更改的。

声明变量时使用 “let” 关键字,例如:

1
let age = 25;

在上面的例子中,我们声明了一个名为 “age” 的变量,并将其初始化为 25。

声明常量时使用 “const” 关键字,例如:

1
const PI = 3.14;

在上面的例子中,我们声明了一个名为 “PI” 的常量,并将其初始化为 3.14。一旦常量被赋值,就无法更改它的值。如果尝试更改常量的值,将会产生错误。

在JavaScript中,变量和常量可以用于存储各种类型的数据,包括数字、字符串、数组、对象等。例如:

1
2
let name = 'Alice';
const daysInWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];

在上面的例子中,我们声明了一个 “name” 变量并将其初始化为字符串类型的 “Alice”,以及一个 “daysInWeek” 常量,其中包含一个字符串数组。

JS中的关键字

以下是 JavaScript 中的关键字:

  • break
  • case
  • catch
  • class
  • const
  • continue
  • debugger
  • default
  • delete
  • do
  • else
  • export
  • extends
  • false
  • finally
  • for
  • function
  • if
  • import
  • in
  • instanceof
  • new
  • null
  • return
  • super
  • switch
  • this
  • throw
  • true
  • try
  • typeof
  • var
  • void
  • while
  • with
  • yield

注意,这个列表并不包括一些 JavaScript 的保留字(如 letasync),因为它们被视为关键字的扩展。

JS中的表达式及其例子

  1. 算术表达式:用于计算数值的表达式,包括加、减、乘、除等操作。

    1
    例子:var sum = 10 + 20;
  2. 字符串表达式:用于处理字符串的表达式,包括字符串连接等操作。

    1
    例子:var greeting = "Hello, " + "World!";
  3. 逻辑表达式:用于处理布尔值的表达式,包括与、或、非等操作。

    1
    例子:var isTrue = (10 > 5) && (5 < 10);
  4. 赋值表达式:用于给变量赋值的表达式。

    1
    例子:var x = 10;
  5. 比较表达式:用于比较两个值的表达式,包括等于、不等于、大于、小于等操作。

    1
    例子:var isEqual = (10 == 10);
  6. 条件(三元)表达式:用于根据条件选择一个值的表达式。

    1
    例子:var max = (10 > 5) ? 10 : 5;
  7. 函数表达式:用于定义一个函数的表达式。

    1
    例子:var square = function(x) { return x * x; };
  8. 对象表达式:用于创建一个对象的表达式。

    1
    例子:var person = { name: "John", age: 30 };
  9. 数组表达式:用于创建一个数组的表达式。

    1
    例子:var numbers = [1, 2, 3, 4, 5];
  10. 正则表达式:用于处理正则表达式的表达式。

    1
    例子:var pattern = /hello/i;

JS内置对象及其案例

JavaScript 有许多内置对象,这些对象可以在任何时候使用。以下是一些常用的内置对象及其使用案例:

  1. Object:Object 是 JavaScript 中所有对象的基类。你可以使用它来创建一个新的对象,或者访问和操作已有对象的属性和方法。

使用案例:

1
2
3
4
let person = new Object();
person.name = "Tom";
person.age = 25;
console.log(person.name); // 输出 "Tom"
  1. Array:Array 对象用于在单个变量中存储多个值。你可以使用它来创建和操作数组。

使用案例:

1
2
3
let fruits = new Array("apple", "banana", "cherry");
console.log(fruits[0]); // 输出 "apple"
fruits.push("orange"); // 在数组末尾添加一个新元素
  1. String:String 对象表示文本数据。你可以使用它来处理字符串。

使用案例:

1
2
3
let str = new String("Hello, world!");
console.log(str.length); // 输出 13
console.log(str.toUpperCase()); // 输出 "HELLO, WORLD!"
  1. Number:Number 对象表示数字值。你可以使用它来处理数字和进行数学运算。

使用案例:

1
2
let num = new Number(42);
console.log(num.toString(16)); // 输出 "2a"(将数字转换为十六进制字符串)
  1. Math:Math 对象提供了一组用于执行数学运算的方法和常量。

使用案例:

1
2
console.log(Math.PI); // 输出 3.141592653589793
console.log(Math.sqrt(16)); // 输出 4(计算平方根)
  1. Date:Date 对象表示日期和时间。你可以使用它来获取和设置日期、时间等。

使用案例:

1
2
3
let now = new Date();
console.log(now.getFullYear()); // 输出当前年份
console.log(now.getMonth() + 1); // 输出当前月份(注意:月份从 0 开始,所以需要加 1)
  1. JSON:JSON 对象用于处理 JSON 数据。你可以使用它来将 JavaScript 对象转换为 JSON 字符串,或将 JSON 字符串解析为 JavaScript 对象。

使用案例:

1
2
3
4
5
6
let obj = {name: "Tom", age: 25};
let jsonString = JSON.stringify(obj); // 将对象转换为 JSON 字符串
console.log(jsonString); // 输出 '{"name":"Tom","age":25}'

let parsedObj = JSON.parse(jsonString); // 将 JSON 字符串解析为对象
console.log(parsedObj.name); // 输出 "Tom"
  1. RegExp:RegExp 对象表示正则表达式。你可以使用它来进行字符串的模式匹配和替换操作。

使用案例:

1
2
3
4
let regex = new RegExp("\\d+", "g"); // 匹配一个或多个数字字符
let str = "There are 25 cats and 42 dogs.";
let result = str.match(regex); // 使用正则表达式匹配字符串
console.log(result); // 输出 ["25", "42"]
  1. Function:Function 对象表示 JavaScript 函数。你可以使用它来创建和调用函数。

使用案例:

1
2
let add = new Function("a", "b", "return a + b;");
console.log(add(1, 2)); // 输出 3
  1. Promise:Promise 对象表示一个异步操作的最终结果。你可以使用它来处理异步操作,例如 AJAX 请求、定时器等。

使用案例:

1
2
3
4
5
6
7
8
9
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hello, world!");
}, 1000);
});

promise.then((value) => {
console.log(value); // 输出 "Hello, world!"(在 1 秒后)
});

这些内置对象为 JavaScript 提供了丰富的功能,使得开发者能够更方便地处理各种任务。

JS本地对象

在JavaScript中,本地对象是指那些内置于JavaScript语言中的对象。这些对象在任何时候都可以在代码中使用,无需额外的引用或导入。以下是一些常见的JavaScript本地对象:

  1. Object:这是所有对象的基类,包含了一些通用的属性和方法,如toString()、valueOf()等。
  2. Array:表示数组类型的对象,提供了一系列操作数组的方法,如push()、pop()、splice()等。
  3. String:表示字符串类型的对象,提供了一系列操作字符串的方法,如charAt()、substring()、trim()等。
  4. Number:表示数字类型的对象,提供了一系列操作数字的方法,如toFixed()、toExponential()等。
  5. Boolean:表示布尔类型的对象,提供了一系列操作布尔值的方法,如valueOf()等。
  6. Date:表示日期和时间的对象,提供了一系列操作日期和时间的方法,如getFullYear()、getMonth()、getDate()等。
  7. RegExp:表示正则表达式的对象,提供了一系列操作正则表达式的方法,如exec()、test()等。
  8. Function:表示函数类型的对象,提供了一系列操作函数的方法,如call()、apply()、bind()等。
  9. Error:表示错误类型的对象,提供了一系列操作错误的方法,如message、name等。
  10. Math:提供了一系列数学计算的方法,如abs()、ceil()、floor()、random()等。
  11. JSON:提供了一系列操作JSON数据的方法,如parse()、stringify()等。
  12. Promise:表示异步操作的对象,提供了一系列操作异步操作的方法,如then()、catch()、finally()等。

这些本地对象为开发者提供了丰富的功能,使得JavaScript编程更加方便和高效。

JS中的事件及其案例

JavaScript中的事件是指在浏览器或者用户操作过程中发生的一些特定交互行为,例如点击、滚动、按键等。通过在JavaScript代码中监听和处理这些事件,我们可以实现对用户操作的响应,从而提高网页的交互性。

以下是一些常见的JavaScript事件及其使用案例:

  1. click事件:当用户点击某个元素时触发。

案例:点击按钮显示/隐藏一个div元素。

1
2
3
4
5
6
7
8
9
10
11
<button id="toggleButton">显示/隐藏</button>
<div id="content" style="display:none;">这是一个隐藏的内容。</div>

<script>
const toggleButton = document.getElementById('toggleButton');
const content = document.getElementById('content');

toggleButton.addEventListener('click', () => {
content.style.display = content.style.display === 'none' ? 'block' : 'none';
});
</script>
  1. mouseover和mouseout事件:当鼠标指针移动到元素上方或离开元素时触发。

案例:鼠标悬停在图片上时放大,离开时恢复原大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<img id="image" src="example.jpg" width="200" height="200" />

<script>
const image = document.getElementById('image');

image.addEventListener('mouseover', () => {
image.width = 300;
image.height = 300;
});

image.addEventListener('mouseout', () => {
image.width = 200;
image.height = 200;
});
</script>
  1. keyup和keydown事件:当用户按下或松开某个键盘按键时触发。

案例:实现一个简单的搜索框,当用户输入内容时,实时显示搜索结果。

1
2
3
4
5
6
7
8
9
10
11
12
<input id="searchInput" type="text" placeholder="输入关键词搜索" />
<div id="searchResult"></div>

<script>
const searchInput = document.getElementById('searchInput');
const searchResult = document.getElementById('searchResult');

searchInput.addEventListener('keyup', () => {
const keyword = searchInput.value;
searchResult.innerHTML = `搜索结果:${keyword}`;
});
</script>
  1. scroll事件:当用户滚动页面时触发。

案例:当页面滚动到一定距离时,显示一个返回顶部的按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<button id="backToTop" style="display:none;position:fixed;bottom:20px;right:20px;">返回顶部</button>

<script>
const backToTop = document.getElementById('backToTop');

window.addEventListener('scroll', () => {
if (window.scrollY > 200) {
backToTop.style.display = 'block';
} else {
backToTop.style.display = 'none';
}
});

backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
</script>

这些只是JavaScript事件的一些简单示例,实际应用中可能会涉及到更多的事件类型和更复杂的交互逻辑。

web答疑

一、选择

1.html结构样式:行内、块常见元素,按常见元素,滚动文字

行内元素:a、span、img、input、label、select、textarea、button、abbr、acronym、cite、code、dfn、em、kbd、q、samp、strong、sub、sup、time、var等 ;

块状元素:div、p、h1~h6、ul、ol、li、dl、dt、dd、blockquote、hr、pre、address、fieldset、form等。

2.样式:选择器6个

标签选择器、id选择器、类选择器、伪类选择器、后代选择器、属性选择器

3.盒子模型(结构图) 外边距,内边距内容等,外边距四个属性值顺序(顺/逆)

HTML 盒子模型(结构图)包括四个部分:

  1. Content(内容区):指 HTML 元素的具体内容,如文本、图片等。
  2. Padding(内边距):指 Content 与 Border 之间的距离,可以用来设置元素内部空间。
  3. Border(边框):指围绕 Content 和 Padding 的边框线,可以用来设置元素边框样式、宽度、颜色等。
  4. Margin(外边距):指 Border 与相邻元素之间的距离,可以用来设置元素与其他元素之间的间距。

下面是 HTML 盒子模型的结构图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+----------------------------------------+
| Margin |
| |
| +---------------------------+ |
| | Border | |
| | | |
| | +--------------------+ | |
| | | Padding | | |
| | | | | |
| | | Content | | |
| | | | | |
| | +--------------------+ | |
| | | |
| +---------------------------+ |
| |
+----------------------------------------+

外边距顺序:上、右、下、左;上下、左右;上下左右;

4.CSS3新的属性:拉伸,旋转,动画

CSS3引入了许多新的属性,其中包括了拉伸、旋转和动画等。

  1. 拉伸(scale):通过scale()函数可以实现对元素进行缩放,可以缩小或放大元素。例如:transform: scale(0.5, 0.5); 可以让元素的大小缩小到原来的一半。

  2. 旋转(rotate):通过rotate()函数可以实现对元素进行旋转,可以顺时针或逆时针旋转。例如:transform: rotate(45deg); 可以让元素按照顺时针方向旋转45度。

  3. 动画(animation):通过animation属性可以实现对元素进行动画效果的控制。可以设置动画的持续时间、延迟时间、重复次数、动画类型等。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .box {
    animation: myanimation 2s ease-in-out infinite;
    }

    @keyframes myanimation {
    0% { background-color: red; }
    50% { background-color: green; }
    100% { background-color: blue; }
    }

    这段代码可以让一个名为“box”的元素,按照myanimation动画效果进行无限循环播放。其中@keyframes定义了动画的关键帧,从开始到50%时背景色渐变为绿色,从50%到结束时背景色渐变为蓝色。

5.js:表达式,运算符

表达式:

  1. 算数表达式:

2 + 3:加法运算符,返回 5。

5 - 2:减法运算符,返回 3。

7 * 4:乘法运算符,返回 28。

10 / 2:除法运算符,返回 5。

9 % 2:取模运算符,返回 1。

  1. 字符串表达式:

"Hello" + " " + "World":加法运算符,返回 “Hello World”。

"My name is " + name:变量 name 存储了一个字符串,加法运算符,返回 “My name is John”。

  1. 布尔表达式:

5 > 3:大于运算符,返回 true。

10 <= 5:小于等于运算符,返回 false。

true && false:逻辑与运算符,返回 false。

!(x > y):逻辑非运算符,如果 x 大于 y 返回 false,否则返回 true。

  1. 条件表达式:

age >= 18 ? "成年人" : "未成年人":如果 age 大于等于 18,返回 “成年人”,否则返回 “未成年人”。

  1. 赋值表达式:

x = 5:将 5 赋值给变量 x。

y += 2:将 y 的值加上 2,并将结果赋值给 y。

  1. 函数调用表达式:

Math.max(2, 5, 8):调用 Math.max 函数,返回最大值 8。

运算符:

  1. 算数运算符:加号、减号、乘号、除号、取模(%)等。
  2. 比较运算符:大于号、小于号、等于号、不等于号、大于等于号、小于等于号等。
  3. 逻辑运算符:与(&&)、或(||)、非(!)等。
  4. 位运算符:按位与(&)、按位或(|)、按位非(~)、按位异或(^)等。
  5. 赋值运算符:等号、加等于(+=)、减等于(-=)、乘等于(*=)、除等于(/=)等。
  6. 条件运算符:问号(?)和冒号(:),用于条件判断。例:2>3?true:flase;
  7. 其他运算符:typeof(返回变量类型)、instanceof(判断对象是否属于某个类)等。

6.消息框:警告框,提示框,确认框

JavaScript 提供了三种常见的消息框:警告框、提示框和确认框。

  1. 警告框

警告框用于向用户显示一条警告消息。它只有一个“确定”按钮,点击后警告框就会消失。警告框的语法如下:

1
alert("消息内容");

其中,消息内容是要向用户显示的消息字符串。例如:

1
alert("请注意,这是一条警告消息!");
  1. 提示框

提示框用于向用户显示一条消息,并接受用户输入数据。

1
prompt("消息内容", "默认值");

其中,消息内容是要向用户显示的消息字符串,而默认值则是一个可选的参数,用于指定用户输入框的默认值。例如:

1
2
var name = prompt("请输入您的姓名:", "");
alert("您好," + name + "!");
  1. 确认框
1
confirm("消息内容");

其中,消息内容是要向用户显示的消息字符串。例如:

1
2
3
4
5
6
7
if (confirm("您确定要删除这个文件吗?")) {
// 用户点击了“确定”按钮
deleteFile();
} else {
// 用户点击了“取消”按钮
return;
}

7.document对象属性方法

属性:

  • document.getElementById(id):根据元素的 id 属性获取元素对象。
  • document.getElementsByClassName() :根据元素的class属性获取元素对象。
  • document.getElementsByTagName(tagname) :根据元素的标签名获取元素对象。
  • document.body:获取 body 元素对象。
  • document.head:获取 head 元素对象。
  • document.title:获取或设置文档的标题。
  • document.URL:获取文档的URL。

方法:

  • document.createElement(tagName):创建一个 HTML 元素。
  • document.createTextNode(data):创建一个包含指定文本内容的文本节点。
  • document.querySelector(selector):根据 CSS 选择器获取第一个匹配的元素。
  • document.querySelectorAll(selector):根据 CSS 选择器获取所有匹配的元素。
  • document.write(content):向文档写入 HTML 内容(会覆盖整个文档)。
  • document.writeln(content):向文档写入 HTML 内容并换行(会覆盖整个文档)。

二、填空:

1.标记的概念

HTML 中的标记是用来描述文档结构和内容的符号,也称为标签(tag)。标记由一对尖括号(<>)包围,包括起始标记和结束标记,例如:

1
<p>这是一个段落。</p>

其中,<p> 是起始标记,用于表示一个段落的开始,</p> 是结束标记,用于表示该段落的结束。标记中间的文本是该标记所描述的内容。

HTML 标记可以分为两类:块级标记和内联标记。

1.块级标记

块级标记用于组织文档的结构,通常表示文档中的一个大块内容,如段落、标题、列表、表格等。块级标记会在页面中单独占据一行,可以设置宽度、高度、边距、内边距等样式。常用的块级标记包括:

  • <h1><h6>:表示六级标题。
  • <p>:表示段落。
  • <div>:表示文档中的一个分割区域,可用于布局、包裹块级元素等。
  • <ul><ol>:表示无序列表和有序列表。
  • <li>:表示列表项。
  • <table>:表示表格。
  • <tr>:表示表格中的行。
  • <td>:表示表格中的单元格。

2.内联标记

内联标记用于标记文本中的一部分内容,通常表示文本中的一个小段落或一个词语,如加粗、斜体、链接等。内联标记不会独占一行,不能设置宽度、高度等样式。常用的内联标记包括:

  • <b><strong>:表示加粗。
  • <i><em>:表示斜体。
  • <u><ins>:表示下划线。
  • <a>:表示链接。
  • <span>:表示文档中的一个小的分割区域,可用于包裹内联元素等。

除了这些常用的标记外,还有其他许多标记,可以根据需要灵活使用。

2.列表的概念。常见列表,有序,无序

列表是一种在网页中展示内容的方式,常见的列表类型包括有序列表和无序列表。

1.无序列表

无序列表是一种用于展示信息的列表类型,其列表项通常以特定符号(如圆点、方块等)开头。在 HTML 中,无序列表使用 <ul> 标签来定义,每个列表项使用 <li> 标签来表示。例如:

1
2
3
4
5
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>

上面的例子中,<ul> 标签定义了一个无序列表,其中包含三个列表项,分别是苹果、香蕉和橙子。

2.有序列表

有序列表是一种用于展示有序信息的列表类型,其列表项通常以数字或字母开头。在 HTML 中,有序列表使用 <ol> 标签来定义,每个列表项使用 <li> 标签来表示。例如:

1
2
3
4
5
<ol>
<li>第一步</li>
<li>第二步</li>
<li>第三步</li>
</ol>

上面的例子中,<ol> 标签定义了一个有序列表,其中包含三个列表项,分别是第一步、第二步和第三步。

需要注意的是,有序列表的列表项默认以数字开头,可以通过 type 属性来指定列表项的开头类型。例如:

1
2
3
4
5
<ol type="A">
<li>第一步</li>
<li>第二步</li>
<li>第三步</li>
</ol>

上面的例子中,type="A" 表示列表项以大写字母(A、B、C…)开头。

除了 type 属性外,还可以使用 start 属性来指定列表项的起始值,以及 reversed 属性来倒序显示列表项。例如:

1
2
3
4
5
<ol start="4" reversed>
<li>第一步</li>
<li>第二步</li>
<li>第三步</li>
</ol>

上面的例子中,start="4" 表示列表项从 4 开始,reversed 表示列表项倒序显示。

3.js的引入方式,内部和外部

内部引入是指 JavaScript 代码写在 HTML 文件中,可以通过 script 标签来实现。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>内部引入示例</title>
</head>
<body>
<h1>这是一个内部引入示例</h1>
<script>
// JavaScript 代码写在这里
alert("Hello, World!");
</script>
</body>
</html>

外部引入是指 JavaScript 代码写在一个单独的 .js 文件中,通过 script 标签的 src 属性来引入。例如:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>外部引入示例</title>
<!-- 在 head 标签中引入外部 js 文件 -->
<script src="main.js"></script>
</head>
<body>
<h1>这是一个外部引入示例</h1>
</body>
</html>

main.js 文件中输出 “Hello, World!”:

1
document.write("Hello, World!");

4.表格:元素,属性,跨行跨列,表头,标题

表格是网页中常用的展示数据的一种方式,可以使用 HTML 标签 <table><thead><tbody><tfoot><tr><th><td> 来创建。

元素

  • <table>:定义一个表格。
  • <thead>:定义表格的表头。
  • <tbody>:定义表格的主体部分。
  • <tfoot>:定义表格的页脚部分。
  • <tr>:定义表格中的一行。
  • <th>:定义表格中的表头单元格。
  • <td>:定义表格中的数据单元格。

属性

  • border:定义表格边框的宽度。
  • cellspacing:定义单元格之间的空白距离。
  • cellpadding:定义单元格内容与单元格边框之间的空白距离。
  • colspan:定义单元格要跨越的列数。
  • rowspan:定义单元格要跨越的行数。

跨行跨列

使用 colspanrowspan 属性可以让单元格跨越多行或多列。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<table border="1">
<tr>
<th>姓名</th>
<th>年龄</th>
<th colspan="2">联系方式</th>
</tr>
<tr>
<td>张三</td>
<td>20</td>
<td>电话</td>
<td>邮箱</td>
</tr>
<tr>
<td rowspan="2">李四</td>
<td>21</td>
<td>电话</td>
<td>邮箱</td>
</tr>
<tr>
<td>地址</td>
<td>邮编</td>
</tr>
</table>

上面的例子中,第一行的第三、四个单元格使用了 colspan="2",表示要跨越两个列;第三行的第一列使用了 rowspan="2",表示要跨越两个行。

表头和标题

使用 <thead> 可以定义表格的表头,使用 <caption> 可以定义表格的标题。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<table border="1">
<caption>学生信息表</caption>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
<tr>
<td>张三</td>
<td>20</td>
<td></td>
</tr>
<tr>
<td>李四</td>
<td>21</td>
<td></td>
</tr>
</tbody>
</table>

三、简答

1.js的标识符,什么是标识符,定义时需要注意什么问题

JavaScript 中标识符是指:用于命名变量、函数或属性的字符序列。标识符可以包含字母、数字、下划线和美元符号,但不能以数字开头。JavaScript 中的标识符是区分大小写的。

例如,以下是合法的标识符:

1
2
3
4
var myVariable = 123;
function myFunction() { ... }
var $myVar = "abc";
var _myVar = "xyz";

定义标识符时需要注意以下几个问题:

  1. 标识符不能以数字开头,只能以字母、下划线或美元符号开头。
  2. 标识符不能包含空格和标点符号,只能包含字母、数字、下划线和美元符号。
  3. 标识符应该使用有意义的名称,以便于代码的可读性和维护性。
  4. 标识符应该采用一致的命名规范,如驼峰命名法等。

总之,在 JavaScript 中定义标识符时需要注意遵守语法规范和命名规范,以便于编写出易于维护和阅读的代码。

2.dom,bom的区别,优点缺点。

区别:

  1. DOM描述了网页内容(如HTML、XML等),并定义了一种访问和操作这些内容的方法和接口。DOM的核心是一棵树形结构,由节点和对象组成,它表示了网页中所有的元素和标签以及它们之间的关系。
  2. BOM则提供了与浏览器窗口交互的对象和方法,例如window对象、location对象、history对象、navigator对象、screen对象等。BOM使得JavaScript能够控制浏览器的行为,例如打开新窗口、改变URL、导航历史记录等。

优缺点:

DOM优:

  • 构建标准统一,不同浏览器的 DOM API 基本一致,开发者可以很方便地编写跨浏览器兼容的代码。
  • 可扩展性好,开发者可以通过自定义 DOM 元素和事件来扩展 HTML 的功能。
  • 操作简单、灵活,开发者可以通过 DOM API 快速地访问和修改 HTML 元素和文本内容。

DOM缺:

  • 操作 DOM 元素可能会导致浏览器重新渲染整个页面,性能较低。
  • 在大型和复杂的页面中,DOM 的树形结构可能会变得非常庞大,导致内存占用过高。

BOM优:

  • 提供了对浏览器窗口和页面的完整访问和操作,使得开发者可以更好地管理浏览器状态和行为。
  • 可以实现浏览器级别的功能,例如打开新窗口、弹出对话框、控制浏览器历史记录等。

BOM缺:

  • 不同浏览器之间的 BOM API 实现可能不同,开发者需要进行兼容性处理。
  • BOM 带来了一些安全隐患,例如可能会被用于实现恶意弹窗等行为。

3.div的用法

  1. 页面布局:通过 div 可以将页面划分为多个区块,每个区块可以对应不同的页面部分,例如头部、导航、侧边栏、主体内容、页脚等。
  2. 包裹内容:使用 div 可以将一组相关元素包裹起来,便于在样式表中对其进行统一的样式设置,或者使用 JavaScript 对其进行统一的操作。
  3. 容器元素:通过 div 可以创建一个空白的容器元素,用于在其中动态添加其他元素。
  4. 响应式布局:在响应式设计中,使用 div 可以对不同的屏幕大小进行布局调整,使得页面在不同的设备上都可以得到良好的展示效果。

4.Web的工作原理

  1. 客户端浏览器向服务器发送 HTTP 请求,请求包括所需资源的 URL 和其他相关信息。
  2. 服务器接收到请求后,根据 URL 找到对应的资源并生成 HTTP 响应,响应包括状态码、消息头和消息体。
  3. 响应被传输回客户端浏览器,浏览器解析响应并渲染页面。如果响应是 HTML 页面,则浏览器会解析 HTML 标记,并使用 CSS 样式和 JavaScript 脚本来渲染和交互。
  4. 在页面渲染期间,浏览器可能会发起其他 HTTP 请求获取页面中引用的其他资源,如图片、样式表、JavaScript 文件等。
  5. 服务器再次响应这些请求,浏览器将这些资源嵌入到页面中或者执行对应的代码。

5.html、js\css在web中的各自的作用

  1. HTML(超文本标记语言):负责页面的内容展示和结构,包括文字、图片、链接、表单等元素的定义和布局。

  2. CSS(层叠样式表):负责页面的样式设计,包括字体、颜色、大小、布局、动画等多个方面,使页面呈现出美观的视觉效果。

  3. JS(JavaScript):负责页面的交互和动态效果,包括用户输入与响应、数据处理、动态操作 DOM、AJAX 等功能。

四、课后题、bs和cs的区别,优缺点

区别:

  1. 应用场景不同

    BS 架构模式主要应用于 Web 应用程序的开发,客户端通过浏览器访问服务器的 Web 应用,而 CS 架构模式主要应用于桌面应用程序和移动应用程序的开发,客户端通过本地安装的软件连接服务器进行交互。

  2. 数据处理方式不同

    BS 架构模式的数据处理主要在服务器端进行,客户端只负责展示和传输数据,而 CS 架构模式的数据处理则主要在客户端进行,服务器只负责存储和提供数据。

  3. 安全性不同

    BS 架构模式相对于 CS 架构模式来说更加安全,因为所有的数据都存储在服务器端,客户端只是通过浏览器访问数据,不会直接接触到数据。而 CS 架构模式需要在客户端本地存储数据,容易受到恶意攻击。

  4. 维护成本和易用性不同

    BS 架构模式相对于 CS 架构模式来说维护成本更低,因为只需要维护服务器端的应用程序,而客户端的浏览器可以自动升级。而 CS 架构模式需要在客户端安装软件,对于用户来说不太友好,还需要不断升级维护。

B/S优缺点

  • 优点:安全性高、维护成本低、易于升级和扩展

  • 缺点:客户端的交互体验和响应速度较慢

C/S优缺点

  • 优点:交互体验好、响应速度快
  • 缺点:安全性低、维护成本高、易受攻击。

五、程序

1.html,css有序列表无序列表嵌套,通过css列表样式前面项目类型的符号修改上级实验

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style>
*{
padding: 0;
margin: 0;
}
.box{
width: 1200px;
height: 40px;
margin: 0 auto;
background-color: #ffaaff;
border: 4px solid palegreen;
}
.abc .myol{
width: 200px;
height: 32px;
text-align: center;
list-style: none;
float: left;
/* padding: 0 auto; */
border: 1px solid red;

}
ol{
display: none;
margin-top: 12px;
}
ol li{
width: 160px;
height: 20px;
text-align: center;
color: orange;
/* list-style-type: disc; */
border: 1px dashed black;
padding: 20px;
}
.abc li:hover ol{
display: block;
}
</style>
</head>
<body>
<div>
<marquee width="1400px" height="40px" direction="right" scrollamount="25" scrolldelay="1" >
<span>欢迎来到实力至上主义的教室</span>
</marquee>

<div class="box">
<ul class="abc">
<li class="myol">Java</li>
<li class="myol">Web</li>
<li class="myol">Spring</li>
<li class="myol">SpringMvc
<ol>
<li type="none">前端控制器</li>
<li type="circle">controller</li>
<li type="disc">dispatchServlet</li>
</ol>
</li>
<li class="myol">myBatis
<ol start="5">
<li type="A">持久层</li>
<li>动态sql</li>
<li>连接简单</li>
</ol>
</li>
</ul>
</div>
</div>
</body>
</html>

2.注册的页面,里面js加上校验,长度的校验(实验10)

长度校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function checkReg(){
let myname=f.yhm.value;
let mypwd=f.pwd.value;
console.log(myname.length);
if(myname=="" || myname.length<6||myname.length>10){
alert("用户名长度应是6-10个字符");
f.yhm.focus();
return false;
}
if(mypwd=="" || mypwd.length<6||mypwd.length>10){
alert("密码长度应是6-10个字符");
f.pwd.focus();
return false;
}
return true;
}

正则表达式校验操作,例如校验邮箱、手机号、身份证号码等。

下面是几个常见的校验示例:

  1. 校验邮箱地址
1
2
3
4
5
6
7
function validateEmail(email) {
var reg = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+\.([a-zA-Z])+$/;
return reg.test(email);
}

console.log(validateEmail("example@example.com")); // true
console.log(validateEmail("example@.com")); // false
  1. 校验手机号码
1
2
3
4
5
6
7
function validatePhone(phone) {
var reg = /^1[3456789]\d{9}$/;
return reg.test(phone);
}

console.log(validatePhone("13812345678")); // true
console.log(validatePhone("12345678901")); // false
  1. 校验身份证号码
1
2
3
4
5
6
7
8
function validateIdCard(idCard) {
var reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return reg.test(idCard);
}

console.log(validateIdCard("510722199012123456")); // true
console.log(validateIdCard("51072219901212345X")); // true
console.log(validateIdCard("51072219901212345")); // false

3.函数,嵌套,自定义函数,递归调用。

函数

JavaScript 内置的常用函数有很多,以下列举一些常用的函数:

  1. 字符串相关函数
  • charAt():返回指定位置的字符。
  • substr():返回指定长度的子字符串。
  • slice():返回指定位置的子字符串。
  • concat():将两个或多个字符串拼接起来。
  • toLowerCase():将字符串转换为小写。
  • toUpperCase():将字符串转换为大写。
  • indexOf():返回字符串中指定字符或子字符串的位置。
  • lastIndexOf():返回字符串中最后一个指定字符或子字符串的位置。
  • replace():替换指定字符或子字符串。
  • split():将字符串按照指定字符分割成数组。
  1. 数组相关函数
  • push():在数组末尾添加一个或多个元素。
  • pop():删除数组末尾的元素并返回该元素。
  • shift():删除数组第一个元素并返回该元素。
  • unshift():在数组开头添加一个或多个元素。
  • concat():将两个或多个数组合并成一个新数组。
  • slice():返回指定位置的子数组。
  • splice():删除或插入数组元素。
  • reverse():颠倒数组元素顺序。
  • sort():按照指定顺序排序数组元素。
  • join():将数组元素转换为字符串并连接起来。
  1. 数字相关函数
  • parseInt():将字符串转换为整数。
  • parseFloat():将字符串转换为浮点数。
  • toFixed():将数字保留指定小数位数并返回字符串。
  • toExponential():将数字转换为指数形式并返回字符串。
  • toString():将数字转换为字符串并返回。
  1. 其他常用函数
  • setTimeout():延迟指定时间后执行函数。
  • setInterval():按照指定时间间隔重复执行函数。
  • clearTimeout():取消 setTimeout() 函数的执行。
  • clearInterval():取消 setInterval() 函数的执行。
  • encodeURI():将字符串编码为 URI 字符串。
  • decodeURI():将 URI 字符串解码为普通字符串。
  • encodeURIComponent():将字符串编码为 URI 组件字符串。
  • decodeURIComponent():将 URI 组件字符串解码为普通字符串。

以上仅是一部分 JavaScript 内置常用函数,JavaScript 还有很多其他的内置函数,需要根据具体需求进行选择使用。

自定义函数、递归调用

1
2
3
4
5
6
7
8
9
10
function fibonacci(n) {
if (n <= 2) {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

console.log(fibonacci(10)); // 输出第10项的值
document.getElementById("myspan").innerHTML=fibonacci(10);

4.循环语句,条件嵌套

JavaScript 中常用的循环语句有 for 循环和 while 循环。

  1. for 循环

for 循环用于重复执行一段代码,通常使用在已知循环次数的情况下。

语法:

1
2
3
for (初始化表达式; 条件表达式; 更新表达式) {
// 循环体代码
}
  • 初始化表达式:定义循环变量并赋初值,只在循环开始时执行一次。
  • 条件表达式:定义循环条件,只要条件为真,循环就会继续执行。
  • 更新表达式:更新循环变量的值,通常是加 1,只在循环体执行完毕后执行一次。
  • 循环体代码:需要被重复执行的代码块。

示例:

1
2
3
4
for (var i = 0; i < 5; i++) {
console.log(i);
}
// 输出:0 1 2 3 4
  1. while 循环

while 循环用于重复执行一段代码,通常使用在不知道循环次数的情况下,只要条件为真,循环就会一直执行下去。

语法:

1
2
3
while (条件表达式) {
// 循环体代码
}
  • 条件表达式:定义循环条件,只要条件为真,循环就会继续执行。
  • 循环体代码:需要被重复执行的代码块。

示例:

1
2
3
4
5
6
var i = 0;
while (i < 5) {
console.log(i);
i++;
}
// 输出:0 1 2 3 4
  1. 条件嵌套

在循环语句中可以嵌套条件语句,以根据不同的情况执行不同的代码块。

示例:

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
if (i % 2 === 0) {
console.log(i + " 是偶数");
} else {
console.log(i + " 是奇数");
}
}

以上代码中,使用 for 循环遍历数字 0 到 9,通过 if...else 判断每个数字是偶数还是奇数,然后输出相应的提示信息。

LPC11C14刷卡门锁

[TOC]

产品开发平台

产品使用方法

智能门锁支持RFID刷卡识别,但是卡片需要手动输入初始密码进行授权后才能使用(由于开发板集成高度并且没有多余按键可以使用,所以采用一个按键进行代替)。进行授权的卡片刷卡后会使得风扇开始转动,如果是非法卡,则就会使得引起蜂鸣器报警。进行授权时会在OLED屏幕上面出现相应提示,根据提示进行操作即可。应用界面可以显示当前环境的温度。湿度和光强。在测试时可以通过串口看到读取到的信息。

结构框图

产品运行流程

产品运行界面

程序源码

主函数main.c

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#include "lpc11xx.h"
#include "oled.h"
#include "uart.h"
#include "rfid.h"
#include "stdio.h"
#include "dht11.h"
#include "light.h"

unsigned char keybbuff[20]="@haungqingyaunjain";//支持3个钥匙
//管脚默认输入
//GPIO时钟默认打开

void mydelay(int x)
{
int i,j;
for(i=0;i<x;i++){
for(j=0;j<20000;j++){

}
}
}

void delay(int time)
{
while(time--);
}



/************************

风扇驱动函数
************************/
void faninit()
{
LPC_GPIO0->DIR|=(0x01<<2);//输出

}

void fanon()
{
LPC_GPIO0->DATA&=~(0x01<<2);//低电平
}

void fanoff()
{
LPC_GPIO0->DATA|=(0x01<<2);//高电平
}


/***************************
LED灯驱动函数
****************************/


void led_init(void)
{
LPC_GPIO3->DIR |= 0x01; //输出
}

void led_on(void)
{
LPC_GPIO3->DATA &= ~0x01; //低电平
}

void led_off(void)
{
LPC_GPIO3->DATA |= 0x01; //高电平
}



void led2init()
{
LPC_GPIO3->DIR|=(1<<1);//输出
}

void led2on()
{
LPC_GPIO3->DATA&=~(0x01<<1);//低电平
}

void led2off()
{
LPC_GPIO3->DATA|=(0x01<<1);//高电平
}



///*************************

// 蜂鸣器驱动

//*************************/

//void beepinit()
//{
// LPC_SYSCON->SYSAHBCLKCTRL|=(0x01<<16); //GPIO时钟配置
// LPC_IOCON->R_PIO1_1|=0x01; //GPIO模式配置(针对复用端口)
// LPC_GPIO1->DIR|=(1<<1); //输出
//}


//void beepon()
//{
// int j=0;
// for(j=0;j<10;j++)
// {
// LPC_GPIO1->DATA|=(1<<1);//置1
// delay(5000);
// LPC_GPIO1->DATA&=~(1<<1);//置0
// delay(1000);
// }
//}

//void beepoff()
//{
// LPC_GPIO1->DATA|=(1<<1);//置1
//}




/*****************************

按键驱动
******************************/

void keyinit()
{
LPC_GPIO0->DIR&=~(0x01<<3); //输入
LPC_GPIO0->IS&=~(0x01<<3); //边沿触发
LPC_GPIO0->IBE&=~(0x01<<3); //关闭双边沿触发
LPC_GPIO0->IEV&=~(0x01<<3); //下降沿触发
LPC_GPIO0->IE|=(0x01<<3); //打开中断
NVIC_EnableIRQ(EINT0_IRQn); //中断使能外部中断0
//参数在LPCXX.h;
//core_cm0.h
}





/**************************
pwm驱动
***************************/
void PWMinit()
{
LPC_SYSCON->SYSAHBCLKCTRL|=(0x01<<16);
LPC_SYSCON->SYSAHBCLKCTRL|=(0x01<<10);
LPC_IOCON->R_PIO1_1|=0x03;

LPC_TMR32B1->PR=4800;
LPC_TMR32B1->MR1=10;
LPC_TMR32B1->MR0=5;
LPC_TMR32B1->MCR|=(0x01<<4);
LPC_TMR32B1->EMR|=(0x03<<4);
LPC_TMR32B1->PWMC|=0x01;
}


/*******************

打开蜂鸣器
********************/
void beepstart()
{
LPC_TMR32B1->TCR|=0x01;
}


/*************************
关闭蜂鸣器
*************************/
void beepstop()
{
LPC_TMR32B1->TCR&=~0x01;
}


/***********************************
密码验证函数
验证成功返回1,失败返回0
***********************************/


int keycmp(unsigned char *str)
{
int i=0;
int rnum=0;
int count=0;
for(i=0;i<16;i++)
{
if(keybbuff[i]==str[i+2])
{
count++;
}
}
if(count==16)
rnum=1;
return rnum;
}

/*****************************
开机界面显示函数
*****************************/

void display()
{
char str1[20];
char str2[20];
int wendu,shidu,lightval;
dht11_get(&shidu,&wendu);
snprintf(str1,sizeof(str1),"shidu:%d wendu:%d",shidu,wendu);
lightval=light_get();
snprintf(str2,sizeof(str2),"guangqiang:%d",lightval);
oled_clear_screen();
oled_disp_string(0,0," welcome to here");
oled_disp_string(2,0,str1);
oled_disp_string(4,0,str2);
oled_disp_string(6,0," dream is possible");
}


int main(void)
{
int state;
led_init(); //LED1初始化
oled_init(); //OLED初始化
led2init(); //LED2初始化
rfid_init(); //rfid初始化
uart_init(); //串口初始化
faninit(); //风扇初始化
PWMinit(); //蜂鸣器初始化
keyinit(); //按键初始化
light_init();
dht11_init();
oled_disp_string(0,0," welcome to use it");
oled_disp_string(1,0," group nine");
while(1)
{
state=rfid_state();
if(state) //寻卡
{
printf("找到卡片\r\n"); //串口打印
rfid_read(); //ID号读取
printf("%s\r\n",(buf+2)); //串口打印ID卡号
if(keycmp(buf)==1&&state==1) //ID卡号比较
{
led2off();
fanon();
mydelay(150);
fanoff();
mydelay(10);
}
else
{
beepstart();
mydelay(50);
beepstop();
led2on();
}
display();
}
led_on();
}

}


/********************
按键中断处理函数
********************/


void PIOINT0_IRQHandler()
{
int i=0;
if(LPC_GPIO0->RIS&(0x01<<3))//中断发生标志位判断
{
oled_clear_screen();
oled_disp_string(0,0," mode:add key ");
oled_disp_string(2,0," put card on rfid");
while(rfid_state()==0); //等待卡片被放下
rfid_read();
for(i=0;i<36;i++)
{
keybbuff[i]=buf[i+2];
}
printf("addkeyID:%s\r\n",keybbuff);
// while(rfid_state()==1); //等待卡片被拿起
///*****************************************************
//
//此处代码存在bug,经检测与软件无关,卡片没有拿起,就直接进入
//下面的函数,直接跳过了while()循环,多次修改无效,为硬件问题
//有些东西用while(1)也留不住,就像中断一样,来得突然,离开莫
//名其妙。
//******************************************************/
oled_clear_screen();
oled_disp_string(0,0," add sucessfully");
mydelay(500);
LPC_GPIO0->IC|=(0x01<<3);//清除中断标志位
}
}

温度传感器驱动

dth11.h

1
2
3
4
5
6
7
#ifndef __DHT_H__
#define __DHT_H__

void dht11_init(void);
int dht11_get(int *h, int *t);

#endif

dth11.c

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
#include "LPC11xx.h"

void Delay10uS(void)
{
int i = 45;
while (i--);
}

void Delay10mS(void)
{
int i = 1000;
while (i--)
Delay10uS();
}

void dht11_init(void)
{
int i = 100;

LPC_GPIO1->DIR |= 1 << 5;
LPC_GPIO1->DATA |= 1 << 5;

while (i--)
Delay10mS();
}
//严格根据 dht11时序读取

int dht11_get(int *h, int *t)
{
int i;
int cnt = 0;
unsigned char data[5] = {0};

__disable_irq();

again:
LPC_GPIO1->DIR |= 1 << 5;
LPC_GPIO1->DATA |= 1 << 5;
Delay10mS();
/* 低电平保持18mS */
LPC_GPIO1->DATA &= ~(1 << 5);
Delay10mS();
Delay10mS();
LPC_GPIO1->DATA |= 1 << 5;

LPC_GPIO1->DIR &= ~(1 << 5);
Delay10uS();
Delay10uS();
Delay10uS();
/* 等待DHT11输出低电平 */
if (LPC_GPIO1->DATA & (1 << 5))
goto again;

/* 等待低电平结束,80uS */
while (!(LPC_GPIO1->DATA & (1 << 5))) {
Delay10uS();
++cnt;
}

/* 开始输出高电平,80uS */
cnt = 0;
while (LPC_GPIO1->DATA & (1 << 5)) {
Delay10uS();
++cnt;
}

for (i = 0; i < 40; i++) {
/* 等待低电平结束,50uS */
while (!(LPC_GPIO1->DATA & (1 << 5)));
/* 对高电平进行计时 */
cnt = 0;
while (LPC_GPIO1->DATA & (1 << 5)) {
Delay10uS();
++cnt;
}
if (cnt > 5)
data[i / 8] |= 1 << (7 - i % 8);
cnt = 0;
}

__enable_irq();

Delay10mS();
LPC_GPIO1->DIR |= 1 << 5;
LPC_GPIO1->DATA |= 1 << 5;

if ((unsigned char)(data[0] + data[1] + data[2] + data[3]) != data[4])
return -1;
else {
*h = data[0];
*t = data[2];
return 0;
}
}

OLED显示屏驱动

ascii.h

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#ifndef __ASCII_H__
#define __ASCII_H__

const unsigned char ASCII[241][5]={ // Refer to "Times New Roman" Font Database...
// Basic Characters
{0x00,0x00,0x00,0x00,0x00}, // ( 0) - 0x0020 Space
{0x00,0x00,0x4F,0x00,0x00}, // ( 1) ! - 0x0021 Exclamation Mark
{0x00,0x07,0x00,0x07,0x00}, // ( 2) " - 0x0022 Quotation Mark
{0x14,0x7F,0x14,0x7F,0x14}, // ( 3) # - 0x0023 Number Sign
{0x24,0x2A,0x7F,0x2A,0x12}, // ( 4) $ - 0x0024 Dollar Sign
{0x23,0x13,0x08,0x64,0x62}, // ( 5) % - 0x0025 Percent Sign
{0x36,0x49,0x55,0x22,0x50}, // ( 6) & - 0x0026 Ampersand
{0x00,0x05,0x03,0x00,0x00}, // ( 7) ' - 0x0027 Apostrophe
{0x00,0x1C,0x22,0x41,0x00}, // ( 8) ( - 0x0028 Left Parenthesis
{0x00,0x41,0x22,0x1C,0x00}, // ( 9) ) - 0x0029 Right Parenthesis
{0x14,0x08,0x3E,0x08,0x14}, // ( 10) * - 0x002A Asterisk
{0x08,0x08,0x3E,0x08,0x08}, // ( 11) + - 0x002B Plus Sign
{0x00,0x50,0x30,0x00,0x00}, // ( 12) , - 0x002C Comma
{0x08,0x08,0x08,0x08,0x08}, // ( 13) - - 0x002D Hyphen-Minus
{0x00,0x60,0x60,0x00,0x00}, // ( 14) . - 0x002E Full Stop
{0x20,0x10,0x08,0x04,0x02}, // ( 15) / - 0x002F Solidus
{0x3E,0x51,0x49,0x45,0x3E}, // ( 16) 0 - 0x0030 Digit Zero
{0x00,0x42,0x7F,0x40,0x00}, // ( 17) 1 - 0x0031 Digit One
{0x42,0x61,0x51,0x49,0x46}, // ( 18) 2 - 0x0032 Digit Two
{0x21,0x41,0x45,0x4B,0x31}, // ( 19) 3 - 0x0033 Digit Three
{0x18,0x14,0x12,0x7F,0x10}, // ( 20) 4 - 0x0034 Digit Four
{0x27,0x45,0x45,0x45,0x39}, // ( 21) 5 - 0x0035 Digit Five
{0x3C,0x4A,0x49,0x49,0x30}, // ( 22) 6 - 0x0036 Digit Six
{0x01,0x71,0x09,0x05,0x03}, // ( 23) 7 - 0x0037 Digit Seven
{0x36,0x49,0x49,0x49,0x36}, // ( 24) 8 - 0x0038 Digit Eight
{0x06,0x49,0x49,0x29,0x1E}, // ( 25) 9 - 0x0039 Dight Nine
{0x00,0x36,0x36,0x00,0x00}, // ( 26) : - 0x003A Colon
{0x00,0x56,0x36,0x00,0x00}, // ( 27) ; - 0x003B Semicolon
{0x08,0x14,0x22,0x41,0x00}, // ( 28) < - 0x003C Less-Than Sign
{0x14,0x14,0x14,0x14,0x14}, // ( 29) = - 0x003D Equals Sign
{0x00,0x41,0x22,0x14,0x08}, // ( 30) > - 0x003E Greater-Than Sign
{0x02,0x01,0x51,0x09,0x06}, // ( 31) ? - 0x003F Question Mark
{0x32,0x49,0x79,0x41,0x3E}, // ( 32) @ - 0x0040 Commercial At
{0x7E,0x11,0x11,0x11,0x7E}, // ( 33) A - 0x0041 Latin Capital Letter A
{0x7F,0x49,0x49,0x49,0x36}, // ( 34) B - 0x0042 Latin Capital Letter B
{0x3E,0x41,0x41,0x41,0x22}, // ( 35) C - 0x0043 Latin Capital Letter C
{0x7F,0x41,0x41,0x22,0x1C}, // ( 36) D - 0x0044 Latin Capital Letter D
{0x7F,0x49,0x49,0x49,0x41}, // ( 37) E - 0x0045 Latin Capital Letter E
{0x7F,0x09,0x09,0x09,0x01}, // ( 38) F - 0x0046 Latin Capital Letter F
{0x3E,0x41,0x49,0x49,0x7A}, // ( 39) G - 0x0047 Latin Capital Letter G
{0x7F,0x08,0x08,0x08,0x7F}, // ( 40) H - 0x0048 Latin Capital Letter H
{0x00,0x41,0x7F,0x41,0x00}, // ( 41) I - 0x0049 Latin Capital Letter I
{0x20,0x40,0x41,0x3F,0x01}, // ( 42) J - 0x004A Latin Capital Letter J
{0x7F,0x08,0x14,0x22,0x41}, // ( 43) K - 0x004B Latin Capital Letter K
{0x7F,0x40,0x40,0x40,0x40}, // ( 44) L - 0x004C Latin Capital Letter L
{0x7F,0x02,0x0C,0x02,0x7F}, // ( 45) M - 0x004D Latin Capital Letter M
{0x7F,0x04,0x08,0x10,0x7F}, // ( 46) N - 0x004E Latin Capital Letter N
{0x3E,0x41,0x41,0x41,0x3E}, // ( 47) O - 0x004F Latin Capital Letter O
{0x7F,0x09,0x09,0x09,0x06}, // ( 48) P - 0x0050 Latin Capital Letter P
{0x3E,0x41,0x51,0x21,0x5E}, // ( 49) Q - 0x0051 Latin Capital Letter Q
{0x7F,0x09,0x19,0x29,0x46}, // ( 50) R - 0x0052 Latin Capital Letter R
{0x46,0x49,0x49,0x49,0x31}, // ( 51) S - 0x0053 Latin Capital Letter S
{0x01,0x01,0x7F,0x01,0x01}, // ( 52) T - 0x0054 Latin Capital Letter T
{0x3F,0x40,0x40,0x40,0x3F}, // ( 53) U - 0x0055 Latin Capital Letter U
{0x1F,0x20,0x40,0x20,0x1F}, // ( 54) V - 0x0056 Latin Capital Letter V
{0x3F,0x40,0x38,0x40,0x3F}, // ( 55) W - 0x0057 Latin Capital Letter W
{0x63,0x14,0x08,0x14,0x63}, // ( 56) X - 0x0058 Latin Capital Letter X
{0x07,0x08,0x70,0x08,0x07}, // ( 57) Y - 0x0059 Latin Capital Letter Y
{0x61,0x51,0x49,0x45,0x43}, // ( 58) Z - 0x005A Latin Capital Letter Z
{0x00,0x7F,0x41,0x41,0x00}, // ( 59) [ - 0x005B Left Square Bracket
{0x02,0x04,0x08,0x10,0x20}, // ( 60) \ - 0x005C Reverse Solidus
{0x00,0x41,0x41,0x7F,0x00}, // ( 61) ] - 0x005D Right Square Bracket
{0x04,0x02,0x01,0x02,0x04}, // ( 62) ^ - 0x005E Circumflex Accent
{0x40,0x40,0x40,0x40,0x40}, // ( 63) _ - 0x005F Low Line
{0x01,0x02,0x04,0x00,0x00}, // ( 64) ` - 0x0060 Grave Accent
{0x20,0x54,0x54,0x54,0x78}, // ( 65) a - 0x0061 Latin Small Letter A
{0x7F,0x48,0x44,0x44,0x38}, // ( 66) b - 0x0062 Latin Small Letter B
{0x38,0x44,0x44,0x44,0x20}, // ( 67) c - 0x0063 Latin Small Letter C
{0x38,0x44,0x44,0x48,0x7F}, // ( 68) d - 0x0064 Latin Small Letter D
{0x38,0x54,0x54,0x54,0x18}, // ( 69) e - 0x0065 Latin Small Letter E
{0x08,0x7E,0x09,0x01,0x02}, // ( 70) f - 0x0066 Latin Small Letter F
{0x06,0x49,0x49,0x49,0x3F}, // ( 71) g - 0x0067 Latin Small Letter G
{0x7F,0x08,0x04,0x04,0x78}, // ( 72) h - 0x0068 Latin Small Letter H
{0x00,0x44,0x7D,0x40,0x00}, // ( 73) i - 0x0069 Latin Small Letter I
{0x20,0x40,0x44,0x3D,0x00}, // ( 74) j - 0x006A Latin Small Letter J
{0x7F,0x10,0x28,0x44,0x00}, // ( 75) k - 0x006B Latin Small Letter K
{0x00,0x41,0x7F,0x40,0x00}, // ( 76) l - 0x006C Latin Small Letter L
{0x7C,0x04,0x18,0x04,0x7C}, // ( 77) m - 0x006D Latin Small Letter M
{0x7C,0x08,0x04,0x04,0x78}, // ( 78) n - 0x006E Latin Small Letter N
{0x38,0x44,0x44,0x44,0x38}, // ( 79) o - 0x006F Latin Small Letter O
{0x7C,0x14,0x14,0x14,0x08}, // ( 80) p - 0x0070 Latin Small Letter P
{0x08,0x14,0x14,0x18,0x7C}, // ( 81) q - 0x0071 Latin Small Letter Q
{0x7C,0x08,0x04,0x04,0x08}, // ( 82) r - 0x0072 Latin Small Letter R
{0x48,0x54,0x54,0x54,0x20}, // ( 83) s - 0x0073 Latin Small Letter S
{0x04,0x3F,0x44,0x40,0x20}, // ( 84) t - 0x0074 Latin Small Letter T
{0x3C,0x40,0x40,0x20,0x7C}, // ( 85) u - 0x0075 Latin Small Letter U
{0x1C,0x20,0x40,0x20,0x1C}, // ( 86) v - 0x0076 Latin Small Letter V
{0x3C,0x40,0x30,0x40,0x3C}, // ( 87) w - 0x0077 Latin Small Letter W
{0x44,0x28,0x10,0x28,0x44}, // ( 88) x - 0x0078 Latin Small Letter X
{0x0C,0x50,0x50,0x50,0x3C}, // ( 89) y - 0x0079 Latin Small Letter Y
{0x44,0x64,0x54,0x4C,0x44}, // ( 90) z - 0x007A Latin Small Letter Z
{0x00,0x08,0x36,0x41,0x00}, // ( 91) { - 0x007B Left Curly Bracket
{0x00,0x00,0x7F,0x00,0x00}, // ( 92) | - 0x007C Vertical Line
{0x00,0x41,0x36,0x08,0x00}, // ( 93) } - 0x007D Right Curly Bracket
{0x02,0x01,0x02,0x04,0x02}, // ( 94) ~ - 0x007E Tilde
{0x3E,0x55,0x55,0x41,0x22}, // ( 95) C - 0x0080 <Control>
{0x00,0x00,0x00,0x00,0x00}, // ( 96) - 0x00A0 No-Break Space
{0x00,0x00,0x79,0x00,0x00}, // ( 97) ! - 0x00A1 Inverted Exclamation Mark
{0x18,0x24,0x74,0x2E,0x24}, // ( 98) c - 0x00A2 Cent Sign
{0x48,0x7E,0x49,0x42,0x40}, // ( 99) L - 0x00A3 Pound Sign
{0x5D,0x22,0x22,0x22,0x5D}, // (100) o - 0x00A4 Currency Sign
{0x15,0x16,0x7C,0x16,0x15}, // (101) Y - 0x00A5 Yen Sign
{0x00,0x00,0x77,0x00,0x00}, // (102) | - 0x00A6 Broken Bar
{0x0A,0x55,0x55,0x55,0x28}, // (103) - 0x00A7 Section Sign
{0x00,0x01,0x00,0x01,0x00}, // (104) " - 0x00A8 Diaeresis
{0x00,0x0A,0x0D,0x0A,0x04}, // (105) - 0x00AA Feminine Ordinal Indicator
{0x08,0x14,0x2A,0x14,0x22}, // (106) << - 0x00AB Left-Pointing Double Angle Quotation Mark
{0x04,0x04,0x04,0x04,0x1C}, // (107) - 0x00AC Not Sign
{0x00,0x08,0x08,0x08,0x00}, // (108) - - 0x00AD Soft Hyphen
{0x01,0x01,0x01,0x01,0x01}, // (109) - 0x00AF Macron
{0x00,0x02,0x05,0x02,0x00}, // (110) - 0x00B0 Degree Sign
{0x44,0x44,0x5F,0x44,0x44}, // (111) +- - 0x00B1 Plus-Minus Sign
{0x00,0x00,0x04,0x02,0x01}, // (112) ` - 0x00B4 Acute Accent
{0x7E,0x20,0x20,0x10,0x3E}, // (113) u - 0x00B5 Micro Sign
{0x06,0x0F,0x7F,0x00,0x7F}, // (114) - 0x00B6 Pilcrow Sign
{0x00,0x18,0x18,0x00,0x00}, // (115) . - 0x00B7 Middle Dot
{0x00,0x40,0x50,0x20,0x00}, // (116) - 0x00B8 Cedilla
{0x00,0x0A,0x0D,0x0A,0x00}, // (117) - 0x00BA Masculine Ordinal Indicator
{0x22,0x14,0x2A,0x14,0x08}, // (118) >> - 0x00BB Right-Pointing Double Angle Quotation Mark
{0x17,0x08,0x34,0x2A,0x7D}, // (119) /4 - 0x00BC Vulgar Fraction One Quarter
{0x17,0x08,0x04,0x6A,0x59}, // (120) /2 - 0x00BD Vulgar Fraction One Half
{0x30,0x48,0x45,0x40,0x20}, // (121) ? - 0x00BF Inverted Question Mark
{0x70,0x29,0x26,0x28,0x70}, // (122) `A - 0x00C0 Latin Capital Letter A with Grave
{0x70,0x28,0x26,0x29,0x70}, // (123) 'A - 0x00C1 Latin Capital Letter A with Acute
{0x70,0x2A,0x25,0x2A,0x70}, // (124) ^A - 0x00C2 Latin Capital Letter A with Circumflex
{0x72,0x29,0x26,0x29,0x70}, // (125) ~A - 0x00C3 Latin Capital Letter A with Tilde
{0x70,0x29,0x24,0x29,0x70}, // (126) "A - 0x00C4 Latin Capital Letter A with Diaeresis
{0x70,0x2A,0x2D,0x2A,0x70}, // (127) A - 0x00C5 Latin Capital Letter A with Ring Above
{0x7E,0x11,0x7F,0x49,0x49}, // (128) AE - 0x00C6 Latin Capital Letter Ae
{0x0E,0x51,0x51,0x71,0x11}, // (129) C - 0x00C7 Latin Capital Letter C with Cedilla
{0x7C,0x55,0x56,0x54,0x44}, // (130) `E - 0x00C8 Latin Capital Letter E with Grave
{0x7C,0x55,0x56,0x54,0x44}, // (131) 'E - 0x00C9 Latin Capital Letter E with Acute
{0x7C,0x56,0x55,0x56,0x44}, // (132) ^E - 0x00CA Latin Capital Letter E with Circumflex
{0x7C,0x55,0x54,0x55,0x44}, // (133) "E - 0x00CB Latin Capital Letter E with Diaeresis
{0x00,0x45,0x7E,0x44,0x00}, // (134) `I - 0x00CC Latin Capital Letter I with Grave
{0x00,0x44,0x7E,0x45,0x00}, // (135) 'I - 0x00CD Latin Capital Letter I with Acute
{0x00,0x46,0x7D,0x46,0x00}, // (136) ^I - 0x00CE Latin Capital Letter I with Circumflex
{0x00,0x45,0x7C,0x45,0x00}, // (137) "I - 0x00CF Latin Capital Letter I with Diaeresis
{0x7F,0x49,0x49,0x41,0x3E}, // (138) D - 0x00D0 Latin Capital Letter Eth
{0x7C,0x0A,0x11,0x22,0x7D}, // (139) ~N - 0x00D1 Latin Capital Letter N with Tilde
{0x38,0x45,0x46,0x44,0x38}, // (140) `O - 0x00D2 Latin Capital Letter O with Grave
{0x38,0x44,0x46,0x45,0x38}, // (141) 'O - 0x00D3 Latin Capital Letter O with Acute
{0x38,0x46,0x45,0x46,0x38}, // (142) ^O - 0x00D4 Latin Capital Letter O with Circumflex
{0x38,0x46,0x45,0x46,0x39}, // (143) ~O - 0x00D5 Latin Capital Letter O with Tilde
{0x38,0x45,0x44,0x45,0x38}, // (144) "O - 0x00D6 Latin Capital Letter O with Diaeresis
{0x22,0x14,0x08,0x14,0x22}, // (145) x - 0x00D7 Multiplcation Sign
{0x2E,0x51,0x49,0x45,0x3A}, // (146) O - 0x00D8 Latin Capital Letter O with Stroke
{0x3C,0x41,0x42,0x40,0x3C}, // (147) `U - 0x00D9 Latin Capital Letter U with Grave
{0x3C,0x40,0x42,0x41,0x3C}, // (148) 'U - 0x00DA Latin Capital Letter U with Acute
{0x3C,0x42,0x41,0x42,0x3C}, // (149) ^U - 0x00DB Latin Capital Letter U with Circumflex
{0x3C,0x41,0x40,0x41,0x3C}, // (150) "U - 0x00DC Latin Capital Letter U with Diaeresis
{0x0C,0x10,0x62,0x11,0x0C}, // (151) `Y - 0x00DD Latin Capital Letter Y with Acute
{0x7F,0x12,0x12,0x12,0x0C}, // (152) P - 0x00DE Latin Capital Letter Thom
{0x40,0x3E,0x01,0x49,0x36}, // (153) B - 0x00DF Latin Capital Letter Sharp S
{0x20,0x55,0x56,0x54,0x78}, // (154) `a - 0x00E0 Latin Small Letter A with Grave
{0x20,0x54,0x56,0x55,0x78}, // (155) 'a - 0x00E1 Latin Small Letter A with Acute
{0x20,0x56,0x55,0x56,0x78}, // (156) ^a - 0x00E2 Latin Small Letter A with Circumflex
{0x20,0x55,0x56,0x55,0x78}, // (157) ~a - 0x00E3 Latin Small Letter A with Tilde
{0x20,0x55,0x54,0x55,0x78}, // (158) "a - 0x00E4 Latin Small Letter A with Diaeresis
{0x20,0x56,0x57,0x56,0x78}, // (159) a - 0x00E5 Latin Small Letter A with Ring Above
{0x24,0x54,0x78,0x54,0x58}, // (160) ae - 0x00E6 Latin Small Letter Ae
{0x0C,0x52,0x52,0x72,0x13}, // (161) c - 0x00E7 Latin Small Letter c with Cedilla
{0x38,0x55,0x56,0x54,0x18}, // (162) `e - 0x00E8 Latin Small Letter E with Grave
{0x38,0x54,0x56,0x55,0x18}, // (163) 'e - 0x00E9 Latin Small Letter E with Acute
{0x38,0x56,0x55,0x56,0x18}, // (164) ^e - 0x00EA Latin Small Letter E with Circumflex
{0x38,0x55,0x54,0x55,0x18}, // (165) "e - 0x00EB Latin Small Letter E with Diaeresis
{0x00,0x49,0x7A,0x40,0x00}, // (166) `i - 0x00EC Latin Small Letter I with Grave
{0x00,0x48,0x7A,0x41,0x00}, // (167) 'i - 0x00ED Latin Small Letter I with Acute
{0x00,0x4A,0x79,0x42,0x00}, // (168) ^i - 0x00EE Latin Small Letter I with Circumflex
{0x00,0x4A,0x78,0x42,0x00}, // (169) "i - 0x00EF Latin Small Letter I with Diaeresis
{0x31,0x4A,0x4E,0x4A,0x30}, // (170) - 0x00F0 Latin Small Letter Eth
{0x7A,0x11,0x0A,0x09,0x70}, // (171) ~n - 0x00F1 Latin Small Letter N with Tilde
{0x30,0x49,0x4A,0x48,0x30}, // (172) `o - 0x00F2 Latin Small Letter O with Grave
{0x30,0x48,0x4A,0x49,0x30}, // (173) 'o - 0x00F3 Latin Small Letter O with Acute
{0x30,0x4A,0x49,0x4A,0x30}, // (174) ^o - 0x00F4 Latin Small Letter O with Circumflex
{0x30,0x4A,0x49,0x4A,0x31}, // (175) ~o - 0x00F5 Latin Small Letter O with Tilde
{0x30,0x4A,0x48,0x4A,0x30}, // (176) "o - 0x00F6 Latin Small Letter O with Diaeresis
{0x08,0x08,0x2A,0x08,0x08}, // (177) + - 0x00F7 Division Sign
{0x38,0x64,0x54,0x4C,0x38}, // (178) o - 0x00F8 Latin Small Letter O with Stroke
{0x38,0x41,0x42,0x20,0x78}, // (179) `u - 0x00F9 Latin Small Letter U with Grave
{0x38,0x40,0x42,0x21,0x78}, // (180) 'u - 0x00FA Latin Small Letter U with Acute
{0x38,0x42,0x41,0x22,0x78}, // (181) ^u - 0x00FB Latin Small Letter U with Circumflex
{0x38,0x42,0x40,0x22,0x78}, // (182) "u - 0x00FC Latin Small Letter U with Diaeresis
{0x0C,0x50,0x52,0x51,0x3C}, // (183) 'y - 0x00FD Latin Small Letter Y with Acute
{0x7E,0x14,0x14,0x14,0x08}, // (184) p - 0x00FE Latin Small Letter Thom
{0x0C,0x51,0x50,0x51,0x3C}, // (185) "y - 0x00FF Latin Small Letter Y with Diaeresis
{0x1E,0x09,0x09,0x29,0x5E}, // (186) A - 0x0104 Latin Capital Letter A with Ogonek
{0x08,0x15,0x15,0x35,0x4E}, // (187) a - 0x0105 Latin Small Letter A with Ogonek
{0x38,0x44,0x46,0x45,0x20}, // (188) 'C - 0x0106 Latin Capital Letter C with Acute
{0x30,0x48,0x4A,0x49,0x20}, // (189) 'c - 0x0107 Latin Small Letter C with Acute
{0x38,0x45,0x46,0x45,0x20}, // (190) C - 0x010C Latin Capital Letter C with Caron
{0x30,0x49,0x4A,0x49,0x20}, // (191) c - 0x010D Latin Small Letter C with Caron
{0x7C,0x45,0x46,0x45,0x38}, // (192) D - 0x010E Latin Capital Letter D with Caron
{0x20,0x50,0x50,0x7C,0x03}, // (193) d' - 0x010F Latin Small Letter D with Caron
{0x1F,0x15,0x15,0x35,0x51}, // (194) E - 0x0118 Latin Capital Letter E with Ogonek
{0x0E,0x15,0x15,0x35,0x46}, // (195) e - 0x0119 Latin Small Letter E with Ogonek
{0x7C,0x55,0x56,0x55,0x44}, // (196) E - 0x011A Latin Capital Letter E with Caron
{0x38,0x55,0x56,0x55,0x18}, // (197) e - 0x011B Latin Small Letter E with Caron
{0x00,0x44,0x7C,0x40,0x00}, // (198) i - 0x0131 Latin Small Letter Dotless I
{0x7F,0x48,0x44,0x40,0x40}, // (199) L - 0x0141 Latin Capital Letter L with Stroke
{0x00,0x49,0x7F,0x44,0x00}, // (200) l - 0x0142 Latin Small Letter L with Stroke
{0x7C,0x08,0x12,0x21,0x7C}, // (201) 'N - 0x0143 Latin Capital Letter N with Acute
{0x78,0x10,0x0A,0x09,0x70}, // (202) 'n - 0x0144 Latin Small Letter N with Acute
{0x7C,0x09,0x12,0x21,0x7C}, // (203) N - 0x0147 Latin Capital Letter N with Caron
{0x78,0x11,0x0A,0x09,0x70}, // (204) n - 0x0148 Latin Small Letter N with Caron
{0x38,0x47,0x44,0x47,0x38}, // (205) "O - 0x0150 Latin Capital Letter O with Double Acute
{0x30,0x4B,0x48,0x4B,0x30}, // (206) "o - 0x0151 Latin Small Letter O with Double Acute
{0x3E,0x41,0x7F,0x49,0x49}, // (207) OE - 0x0152 Latin Capital Ligature Oe
{0x38,0x44,0x38,0x54,0x58}, // (208) oe - 0x0153 Latin Small Ligature Oe
{0x7C,0x15,0x16,0x35,0x48}, // (209) R - 0x0158 Latin Capital Letter R with Caron
{0x78,0x11,0x0A,0x09,0x10}, // (210) r - 0x0159 Latin Small Letter R with Caron
{0x48,0x54,0x56,0x55,0x20}, // (211) 'S - 0x015A Latin Capital Letter S with Acute
{0x20,0x48,0x56,0x55,0x20}, // (212) 's - 0x015B Latin Small Letter S with Acute
{0x48,0x55,0x56,0x55,0x20}, // (213) S - 0x0160 Latin Capital Letter S with Caron
{0x20,0x49,0x56,0x55,0x20}, // (214) s - 0x0161 Latin Small Letter S with Caron
{0x04,0x05,0x7E,0x05,0x04}, // (215) T - 0x0164 Latin Capital Letter T with Caron
{0x08,0x3C,0x48,0x22,0x01}, // (216) t' - 0x0165 Latin Small Letter T with Caron
{0x3C,0x42,0x45,0x42,0x3C}, // (217) U - 0x016E Latin Capital Letter U with Ring Above
{0x38,0x42,0x45,0x22,0x78}, // (218) u - 0x016F Latin Small Letter U with Ring Above
{0x3C,0x43,0x40,0x43,0x3C}, // (219) "U - 0x0170 Latin Capital Letter U with Double Acute
{0x38,0x43,0x40,0x23,0x78}, // (220) "u - 0x0171 Latin Small Letter U with Double Acute
{0x0C,0x11,0x60,0x11,0x0C}, // (221) "Y - 0x0178 Latin Capital Letter Y with Diaeresis
{0x44,0x66,0x55,0x4C,0x44}, // (222) 'Z - 0x0179 Latin Capital Letter Z with Acute
{0x48,0x6A,0x59,0x48,0x00}, // (223) 'z - 0x017A Latin Small Letter Z with Acute
{0x44,0x64,0x55,0x4C,0x44}, // (224) Z - 0x017B Latin Capital Letter Z with Dot Above
{0x48,0x68,0x5A,0x48,0x00}, // (225) z - 0x017C Latin Small Letter Z with Dot Above
{0x44,0x65,0x56,0x4D,0x44}, // (226) Z - 0x017D Latin Capital Letter Z with Caron
{0x48,0x69,0x5A,0x49,0x00}, // (227) z - 0x017E Latin Small Letter Z with Caron
{0x00,0x02,0x01,0x02,0x00}, // (228) ^ - 0x02C6 Modifier Letter Circumflex Accent
{0x00,0x01,0x02,0x01,0x00}, // (229) - 0x02C7 Caron
{0x00,0x01,0x01,0x01,0x00}, // (230) - 0x02C9 Modifier Letter Macron
{0x01,0x02,0x02,0x01,0x00}, // (231) - 0x02D8 Breve
{0x00,0x00,0x01,0x00,0x00}, // (232) - 0x02D9 Dot Above
{0x00,0x02,0x05,0x02,0x00}, // (233) - 0x02DA Ring Above
{0x02,0x01,0x02,0x01,0x00}, // (234) ~ - 0x02DC Small Tilde
{0x7F,0x05,0x15,0x3A,0x50}, // (235) Pt - 0x20A7 Peseta Sign
{0x3E,0x55,0x55,0x41,0x22}, // (236) C - 0x20AC Euro Sign
{0x18,0x14,0x08,0x14,0x0C}, // (237) - 0x221E Infinity
{0x44,0x4A,0x4A,0x51,0x51}, // (238) < - 0x2264 Less-Than or Equal to
{0x51,0x51,0x4A,0x4A,0x44}, // (239) > - 0x2265 Greater-Than or Equal to
{0x74,0x42,0x41,0x42,0x74}, // (240) - 0x2302 House
};

#endif

spi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef __SPI_H__
#define __SPI_H__

#define OLED 0
#define FLASH 1
#define RFID 2
#define UART 3

void spi_init(int ch, int rate, int mode);
void spi_write_read(int ch, int slave, unsigned char *wbuf, unsigned char *rbuf, unsigned int len);
unsigned char spi_send(int ch, int slave, unsigned char dat);

#endif

spi.c

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
139
140
141
#include <LPC11xx.h>

#include "spi.h"

void spi_init(int ch, int rate, int mode)
{
if (ch == 0) {

LPC_SYSCON->PRESETCTRL |= (0x1<<0);
LPC_SYSCON->SYSAHBCLKCTRL |= (0x1<<11);
LPC_SYSCON->SSP0CLKDIV = 24; /* Divided by 24 */
LPC_IOCON->PIO0_8 &= ~0x07; /* SSP I/O config */
LPC_IOCON->PIO0_8 |= 0x01; /* SSP MISO */
LPC_IOCON->PIO0_9 &= ~0x07;
LPC_IOCON->PIO0_9 |= 0x01; /* SSP MOSI */

LPC_IOCON->SCK_LOC = 0x02;
LPC_IOCON->PIO0_6 = 0x02; /* P0.6 function 2 is SSP clock, need to
combined with IOCONSCKLOC register setting */

LPC_SYSCON->SYSAHBCLKCTRL |= (1<<6);
LPC_IOCON->PIO2_4 &= ~0x07; /* SSP SSEL0 is a GPIO pin, for OLED */
LPC_GPIO2->DIR |= 1 << 4;
LPC_GPIO2->DATA |= 1 << 4;

LPC_IOCON->PIO2_6 &= ~0x07; /* SSP SSEL1 is a GPIO pin, for FLASH */
LPC_GPIO2->DIR |= 1 << 6;
LPC_GPIO2->DATA |= 1 << 6;

LPC_IOCON->PIO2_7 &= ~0x07; /* SSP SSEL2 is a GPIO pin, for RFID */
LPC_GPIO2->DIR |= 1 << 7;
LPC_GPIO2->DATA |= 1 << 7;
} else {
LPC_SYSCON->PRESETCTRL |= (0x1<<2);
LPC_SYSCON->SYSAHBCLKCTRL |= (1<<18);
LPC_SYSCON->SSP1CLKDIV = 24; /* Divided by 24 */
LPC_IOCON->PIO2_2 &= ~0x07; /* SSP I/O config */
LPC_IOCON->PIO2_2 |= 0x02; /* SSP MISO */
LPC_IOCON->PIO2_3 &= ~0x07;
LPC_IOCON->PIO2_3 |= 0x02; /* SSP MOSI */
LPC_IOCON->PIO2_1 &= ~0x07;
LPC_IOCON->PIO2_1 |= 0x02; /* SSP CLK */

LPC_IOCON->PIO2_0 &= ~0x07; /* SSP SSEL is a GPIO pin */
LPC_GPIO2->DIR |= 1 << 0;
LPC_GPIO2->DATA |= 1 << 0;
}

if (ch == 0) {
LPC_SSP0->CPSR = 2; /* Divided by 2 */
LPC_SSP0->CR0 = ((1000000 / rate - 1) << 8) | \
((mode & 1) << 7) | (((mode & 2) >> 1) << 6) | \
(7 << 0);
LPC_SSP0->IMSC = 0; /* disable all interrupts */
LPC_SSP0->ICR = 3; /* clears interrupts */
LPC_SSP0->CR1 = 1 << 1; /* Master, SPI Enable */
} else {
LPC_SSP1->CPSR = 2; /* Divided by 2 */
LPC_SSP1->CR0 = ((1000000 / rate - 1) << 8) | \
((mode & 1) << 7) | (((mode & 2) >> 1) << 6) | \
(7 << 0);
LPC_SSP1->IMSC = 0; /* disable all interrupts */
LPC_SSP1->ICR = 3; /* clears interrupts */
LPC_SSP1->CR1 = 1 << 1; /* Master, SPI Enable */
}
}

void spi_set_cs(int slave)
{
switch (slave) {
case OLED:
LPC_GPIO2->DATA |= 1 << 4;
break;
case FLASH:
LPC_GPIO2->DATA |= 1 << 6;
break;
case RFID:
LPC_GPIO2->DATA |= 1 << 7;
break;
case UART:
LPC_GPIO2->DATA |= 1 << 0;
break;
}
}

void spi_clr_cs(int slave)
{
switch (slave) {
case OLED:
LPC_GPIO2->DATA &= ~(1 << 4);
break;
case FLASH:
LPC_GPIO2->DATA &= ~(1 << 6);
break;
case RFID:
LPC_GPIO2->DATA &= ~(1 << 7);
break;
case UART:
LPC_GPIO2->DATA &= ~(1 << 0);
break;
}
}

void spi_write_read(int ch, int slave, unsigned char *wbuf, unsigned char *rbuf, unsigned int len)
{
int i = 0;
if(ch == 0)
{
while (i < len) {
spi_clr_cs(slave);
while((LPC_SSP0->SR & ((1 << 1) | (1 << 4))) != (1 << 1));
LPC_SSP0->DR = wbuf[i];
while(LPC_SSP0->SR & (1 << 4));

while((LPC_SSP0->SR & ((1 << 2) | (1 << 4))) != (1 << 2));
rbuf[i] = LPC_SSP0->DR;
spi_set_cs(slave);
i++;
}
} else {
while (i < len) {
spi_clr_cs(slave);
while((LPC_SSP1->SR & ((1 << 1) | (1 << 4))) != (1 << 1));
LPC_SSP1->DR = wbuf[i];
while(LPC_SSP1->SR & (1 << 4));

while((LPC_SSP1->SR & ((1 << 2) | (1 << 4))) != (1 << 2));
rbuf[i] = LPC_SSP1->DR;
spi_set_cs(slave);
i++;
}
}
}

unsigned char spi_send(int ch, int slave, unsigned char dat)
{
unsigned char c;

spi_write_read(ch, slave, &dat, &c, 1);
return c;
}

oled.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef __OLED_H__
#define __OLED_H__

typedef unsigned char uint8_t;
typedef unsigned short uint16_t;


void oled_init(void);
void oled_clear_screen(void);
void oled_disp_char(uint8_t Line, uint16_t Column, uint8_t Color, char c);
void oled_disp_string(uint8_t Line, uint8_t Color, char *ptr);

#endif

oled.c

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#include <lpc11xx.h>
#include "spi.h"
#include "ascii.h"

/******************************************************************************/
/* Write command & data */
/******************************************************************************/
void WriteCommand(unsigned char cmd)
{
LPC_GPIO2->DATA &= ~(1 << 5);
spi_write_read(0, OLED, &cmd, &cmd, 1);
LPC_GPIO2->DATA |= 1 << 5;
}

void WriteData(unsigned char dat)
{
LPC_GPIO2->DATA |= 1 << 5;
spi_write_read(0, OLED, &dat, &dat, 1);
LPC_GPIO2->DATA |= 1 << 5;
}

/******************************************************************************/
/* Instruction Setting */
/******************************************************************************/
void SetStartColumn(unsigned char d)
{
WriteCommand(0x00+d%16); // Set Lower Column Start Address for Page Addressing Mode
// Default => 0x00
WriteCommand(0x10+d/16); // Set Higher Column Start Address for Page Addressing Mode
// Default => 0x10
}

void SetAddressingMode(unsigned char d)
{
WriteCommand(0x20); // Set Memory Addressing Mode
WriteCommand(d); // Default => 0x02
// 0x00 => Horizontal Addressing Mode
// 0x01 => Vertical Addressing Mode
// 0x02 => Page Addressing Mode
}

void SetColumnAddress(unsigned char a, unsigned char b)
{
WriteCommand(0x21); // Set Column Address
WriteCommand(a); // Default => 0x00 (Column Start Address)
WriteCommand(b); // Default => 0x7F (Column End Address)
}

void SetPageAddress(unsigned char a, unsigned char b)
{
WriteCommand(0x22); // Set Page Address
WriteCommand(a); // Default => 0x00 (Page Start Address)
WriteCommand(b); // Default => 0x07 (Page End Address)
}

void SetStartLine(unsigned char d)
{
WriteCommand(0x40|d); // Set Display Start Line
// Default => 0x40 (0x00)
}

void SetContrastControl(unsigned char d)
{
WriteCommand(0x81); // Set Contrast Control
WriteCommand(d); // Default => 0x7F
}

void SetChargePump(unsigned char d)
{
WriteCommand(0x8D); // Set Charge Pump
WriteCommand(0x10|d); // Default => 0x10
// 0x10 (0x00) => Disable Charge Pump
// 0x14 (0x04) => Enable Charge Pump
}

void SetSegmentRemap(unsigned char d)
{
WriteCommand(0xA0|d); // Set Segment Re-Map
// Default => 0xA0
// 0xA0 (0x00) => Column Address 0 Mapped to SEG0
// 0xA1 (0x01) => Column Address 0 Mapped to SEG127
}

void SetEntireDisplay(unsigned char d)
{
WriteCommand(0xA4|d); // Set Entire Display On / Off
// Default => 0xA4
// 0xA4 (0x00) => Normal Display
// 0xA5 (0x01) => Entire Display On
}

void SetInverseDisplay(unsigned char d)
{
WriteCommand(0xA6|d); // Set Inverse Display On/Off
// Default => 0xA6
// 0xA6 (0x00) => Normal Display
// 0xA7 (0x01) => Inverse Display On
}

void SetMultiplexRatio(unsigned char d)
{
WriteCommand(0xA8); // Set Multiplex Ratio
WriteCommand(d); // Default => 0x3F (1/64 Duty)
}

void SetDisplayOnOff(unsigned char d)
{
WriteCommand(0xAE|d); // Set Display On/Off
// Default => 0xAE
// 0xAE (0x00) => Display Off
// 0xAF (0x01) => Display On
}

void SetStartPage(unsigned char d)
{
WriteCommand(0xB0|d); // Set Page Start Address for Page Addressing Mode
// Default => 0xB0 (0x00)
}

void SetCommonRemap(unsigned char d)
{
WriteCommand(0xC0|d); // Set COM Output Scan Direction
// Default => 0xC0
// 0xC0 (0x00) => Scan from COM0 to 63
// 0xC8 (0x08) => Scan from COM63 to 0
}

void SetDisplayOffset(unsigned char d)
{
WriteCommand(0xD3); // Set Display Offset
WriteCommand(d); // Default => 0x00
}

void SetDisplayClock(unsigned char d)
{
WriteCommand(0xD5); // Set Display Clock Divide Ratio / Oscillator Frequency
WriteCommand(d); // Default => 0x80
// D[3:0] => Display Clock Divider
// D[7:4] => Oscillator Frequency
}

void SetPrechargePeriod(unsigned char d)
{
WriteCommand(0xD9); // Set Pre-Charge Period
WriteCommand(d); // Default => 0x22 (2 Display Clocks [Phase 2] / 2 Display Clocks [Phase 1])
// D[3:0] => Phase 1 Period in 1~15 Display Clocks
// D[7:4] => Phase 2 Period in 1~15 Display Clocks
}

void SetCommonConfig(unsigned char d)
{
WriteCommand(0xDA); // Set COM Pins Hardware Configuration
WriteCommand(0x02|d); // Default => 0x12 (0x10)
// Alternative COM Pin Configuration
// Disable COM Left/Right Re-Map
}

void SetVCOMH(unsigned char d)
{
WriteCommand(0xDB); // Set VCOMH Deselect Level
WriteCommand(d); // Default => 0x20 (0.77*VCC)
}

void SetNOP(void)
{
WriteCommand(0xE3); // Command for No Operation
}

/******************************************************************************/
/* Global Variables */
/******************************************************************************/
#define XLevelL 0x00
#define XLevelH 0x10
#define XLevel ((XLevelH&0x0F)*16+XLevelL)
#define Max_Column 128
#define Max_Row 64
#define Brightness 0xCF

/******************************************************************************/
/* Show Regular Pattern (Full Screen) */
/******************************************************************************/
void FillRAM(unsigned char Data)
{
unsigned char i,j;

for(i=0;i<8;i++)
{
SetStartPage(i);
SetStartColumn(0x00);

for(j=0;j<128;j++)
{
WriteData(Data);
}
}
}

/******************************************************************************/
/* OLED API */
/******************************************************************************/

void oled_init(void) // VCC Generated by Internal DC/DC Circuit
{
LPC_SYSCON->SYSAHBCLKCTRL |= 1<<16;//只要是复用引脚就得打开IOCON时钟

spi_init(0, 100000, 0);

LPC_GPIO2->DIR |= 1 << 5;

SetDisplayOnOff(0x00); // Display Off (0x00/0x01)
SetDisplayClock(0x80); // Set Clock as 100 Frames/Sec
SetMultiplexRatio(0x3F); // 1/64 Duty (0x0F~0x3F)
SetDisplayOffset(0x00); // Shift Mapping RAM Counter (0x00~0x3F)
SetStartLine(0x00); // Set Mapping RAM Display Start Line (0x00~0x3F)
SetChargePump(0x04); // Enable Embedded DC/DC Converter (0x00/0x04)
SetAddressingMode(0x02); // Set Page Addressing Mode (0x00/0x01/0x02)
SetSegmentRemap(0x01); // Set SEG/Column Mapping (0x00/0x01)
SetCommonRemap(0x08); // Set COM/Row Scan Direction (0x00/0x08)
SetCommonConfig(0x10); // Set Sequential Configuration (0x00/0x10)
SetContrastControl(Brightness); // Set SEG Output Current
SetPrechargePeriod(0xF1); // Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
SetVCOMH(0x40); // Set VCOM Deselect Level
SetEntireDisplay(0x00); // Disable Entire Display On (0x00/0x01)
SetInverseDisplay(0x00); // Disable Inverse Display On (0x00/0x01)

FillRAM(0x00); // Clear Screen

SetDisplayOnOff(0x01); // Display On (0x00/0x01)
}

void oled_clear_screen(void)
{
FillRAM(0x00);
}

/*******************************************************************************
* Function Name : oled_disp_char
* Description : Displays a char on the OLED.
* Input : - Line: the Line where to display the character shape .
* This parameter can be one of the following values:
* - Linex: where x can be 0..7
* - Column: start column address.
* - Color: display color
* - 0: dot is yellow or blue
* - 1: dot is black
* - c: character ascii code, must be between 0x20 and 0x7E.
* Output : None
* Return : None
*******************************************************************************/
void oled_disp_char(uint8_t Line, uint16_t Column, uint8_t Color, char c)
{
uint8_t *Src_Pointer;
uint8_t i;

Src_Pointer = (unsigned char *)&ASCII[c - 32][0];

SetStartPage(Line);
if(Column == 0) {
SetStartColumn(0);
if(Color == 0)
WriteData(0x00);
else
WriteData(0xff);
}
SetStartColumn(Column+1);

for(i = 0; i < 5; i++) {
if(Color == 0)
WriteData(*Src_Pointer);
else
WriteData(~(*Src_Pointer));
Src_Pointer ++;
}

if(Color == 0)
WriteData(0x00);
else
WriteData(0xff);
}

/*******************************************************************************
* Function Name : oled_disp_string
* Description : Displays a maximum of 21 char on the OLED.
* Input : - Line: the Line where to display the character shape .
* This parameter can be one of the following values:
* - Linex: where x can be 0..9
* - Color: display color
* - 0: dot is yellow or blue
* - 1: dot is black
* - *ptr: pointer to string to display on LCD.
* Output : None
* Return : None
*******************************************************************************/
void oled_disp_string(uint8_t Line, uint8_t Color, char *ptr)
{
uint16_t refcolumn = 0;

if(Line < 8) {
while ((*ptr != 0) & (((refcolumn + 1) & 0xFFFF) < 128)) {
oled_disp_char(Line, refcolumn, Color, *ptr);
refcolumn += 6;
ptr++;
}
}
}

串口驱动

uart.h

1
2
3
4
5
6
7
8
9
10
#ifndef __UART__
#define __UART__


void uart_init(void);
int uart_send(char c);
char uart_recv(void);


#endif

uart.c

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
#include <LPC11xx.h>
#include <stdio.h>
#include "uart.h"


void uart_init(void)
{
LPC_SYSCON->SYSAHBCLKCTRL |= 1<<16;


LPC_IOCON->PIO1_6 &= ~0x07; /* UART I/O config */
LPC_IOCON->PIO1_6 |= 0x01; /* UART RXD */
LPC_IOCON->PIO1_7 &= ~0x07;
LPC_IOCON->PIO1_7 |= 0x01; /* UART TXD */

/* Enable UART clock */
LPC_SYSCON->SYSAHBCLKCTRL |= (1<<12);
LPC_SYSCON->UARTCLKDIV = 0x1;

LPC_UART->LCR = 0x83; /* 8 bits, no Parity, 1 Stop bit */

LPC_UART->DLM = 0;
LPC_UART->DLL = 16;
LPC_UART->FDR = 0x85;
//
LPC_UART->LCR = 0x03; /* DLAB = 0 */
LPC_UART->TER = 1<<7;

}

int uart_send(char c)
{
LPC_UART->THR = c;
while (!(LPC_UART->LSR & (1 << 5))){
;
}
return c;
}

char uart_recv(void)
{
while (!(LPC_UART->LSR & (1 << 0))){
;
}

return LPC_UART->RBR;
}

int fputc(int ch, FILE *f)
{
return uart_send(ch);
}

int fgetc(FILE *f)
{
return uart_recv();
}

RFID模块驱动

rfid.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __RFID_H__
#define __RFID_H__

extern unsigned char buf[32];
void rfid_init(void);
int rfid_state(void);
void rfid_read(void);
int rfid_write(unsigned char *wbuf);

#endif




rfid.c

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#include "spi.h"
#include <string.h>
#include <LPC11xx.h>
#include "uart.h"
#include "stdio.h"


unsigned char buf[32];

void delay(unsigned int loop);

const unsigned char RFID_READ_DATA_BLOCK_21[10] = {0x0a, 0x21, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
const unsigned char RFID_WRITE_DATA_BLOCK_22[10] = {0x1a, 0x22, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};


static void delay(unsigned int loop)
{
int i;

while (loop--) {
for (i = 10000; i > 0; i--);
}
}


/**************************
rfid初始化
**************************/

void rfid_init(void)
{

LPC_SYSCON->SYSAHBCLKCTRL |= 1 << 16;
SystemCoreClockUpdate();
SysTick_Config(SystemCoreClock / 500);


spi_init(0, 100000, 0);

LPC_GPIO2->DIR &= ~(1 << 8);
}


/************************
rfid检测卡片函数
************************/

int rfid_state(void)
{
static unsigned int pre = 1, cur = 1;

cur = (LPC_GPIO2->DATA >> 8) & 1;
if (cur != pre && pre == 1) {
pre = cur;
return 1;
} else {
pre = cur;
return 0;
}
}

unsigned char rfid_checksum(const unsigned char *buf)
{
int i;
unsigned char chksum = 0;

for (i = 0; i < buf[0]; i++)
chksum += buf[i];

return chksum;
}


/**********************
rfid读取函数(修改简易版)
原来的函数用不了,就和
STM32cubeIDE一个德行
**********************/

void rfid_read()
{
int i;
unsigned char chksum = rfid_checksum(RFID_READ_DATA_BLOCK_21);

spi_send(0, RFID, 0xAA);
delay(10);
spi_send(0, RFID, 0xBB);
delay(10);
for (i = 0; i < RFID_READ_DATA_BLOCK_21[0]; i++) {
spi_send(0, RFID, RFID_READ_DATA_BLOCK_21[i]);
delay(10);
}
spi_send(0, RFID, chksum);

delay(500);

spi_send(0, RFID, 0xAA);
delay(10);
spi_send(0, RFID, 0xBB);
delay(10);

for (i = 0; i < 19; i++) {
buf[i] = spi_send(0, RFID, 0);
delay(10);
}


// chksum = rfid_checksum(buf);
// if (chksum != buf[buf[0]])
// return -1;
// else {
// memcpy(rbuf, buf + 2, 16);
// return 0;
// }




/****************************
注释代码经过使用串口设置断点得知,会使得程序进入死循环,具体原因不明
并且,全部代码在使用指针返回读取到的ID卡号时,串口没法打印,OLED没法显
示,所以采用全局变量,并在头文件中用extern修饰。调用方式如下:

int rfid_read(unsigned char *rbuf)
{

.......................

}


int main()
{
unsigned char *rbuf;
int state;
rfid_init();
uartinit();
while(1)
{
if(rfid_state())
{
printf("找到卡片");
state=rfid_read(buf);
if(staate==0)
printf("%s",buf);
else if(state===-1)
printf("读取失败");
}
}
}
在显示找到卡片后,代码就处于一个停止状态。
上述代码的正常运行情况应该是,在显示找到卡片之后
如果读取成功就通过串口打印ID,失败就打印,读取失败
,并且是循环打印,但是,这东西是直接死机了。硬件问题
硬件问题
****************************/
}







/********************************
写函数

向ID卡写入数据,通过这个函数;
可以将ID卡读取的信息进行改变;
比如,我们可以将我们所使用的;
money:100修改为,I am cool man;
unsigned char *str="I am cool man"
rfid_write(str);

********************************/
int rfid_write(unsigned char *wbuf)
{
int i;
unsigned char chksum;
unsigned char buf[32];

memcpy(buf, RFID_WRITE_DATA_BLOCK_22, 10);
memcpy(buf + 10, wbuf, 16);
chksum = rfid_checksum(buf);

spi_send(0, RFID, 0xAA);
delay(10);
spi_send(0, RFID, 0xBB);
delay(10);
for (i = 0; i < buf[0]; i++) {
spi_send(0, RFID, buf[i]);
delay(10);
}
spi_send(0, RFID, chksum);

delay(500);

spi_send(0, RFID, 0xAA);
delay(10);
spi_send(0, RFID, 0xBB);
delay(10);

for (i = 0; i < 3; i++) {
buf[i] = spi_send(0, RFID, 0);
delay(10);
}

if (buf[0] != 3 || buf[1] != 0x22)
return -1;
else
return 0;
}




void SysTick_Handler(void)
{

}


光强传感器驱动

i2c.h

1
2
3
4
5
6
7
8
#ifndef __I2C_H__
#define __I2C_H__

void i2c_init(void);
int i2c_write(unsigned char addr, unsigned char reg, unsigned char *buf, int len);
int i2c_read(unsigned char addr, unsigned char reg, unsigned char *buf, int len);

#endif

i2c.c

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
139
140
141
142
143
144
145
146
147
148
149
150
#include <stdbool.h>
#include <LPC11xx.h>

bool i2c_start(void);
bool i2c_stop(void);

void i2c_init(void)
{
LPC_SYSCON->PRESETCTRL |= 1 << 1;
LPC_SYSCON->SYSAHBCLKCTRL |= 1 << 5;

LPC_IOCON->PIO0_4 = 0x01;
LPC_IOCON->PIO0_5 = 0x01;
LPC_I2C->SCLH = 480;
LPC_I2C->SCLL = 480;
LPC_I2C->CONCLR |= (1 << 2) | (1 << 3) | (1 << 5) | (1 << 6);
LPC_I2C->CONSET |= (1 << 6);
}

bool i2c_start(void)
{
LPC_I2C->CONSET = (1 << 5); /* START */
while (!(LPC_I2C->CONSET & (1 << 3))); /* wait interrupt */
if (LPC_I2C->STAT != 0x08 && LPC_I2C->STAT != 0x10) {
i2c_stop();
return false;
}
return true;
}

bool i2c_stop(void)
{
LPC_I2C->CONSET = (1 << 4); /* Set Stop flag */
LPC_I2C->CONCLR = (1 << 3); /* Clear SI flag */
while(LPC_I2C->CONSET & (1 << 4)); /* Wait for STOP detected */
return true;
}

bool i2c_send_sla(unsigned char addr)
{
LPC_I2C->DAT = addr;
LPC_I2C->CONCLR = (1 << 5) | (1 << 3); /* clear interrupt and sta */
while (!(LPC_I2C->CONSET & (1 << 3))); /* wait interrupt */
if (LPC_I2C->STAT != 0x18 && LPC_I2C->STAT != 0x40) {
i2c_stop();
return false;
} else
return true;
}

bool i2c_send_dat(unsigned char dat)
{
LPC_I2C->DAT = dat;
LPC_I2C->CONCLR = (1 << 5) | (1 << 3); /* clear interrupt and sta */
while (!(LPC_I2C->CONSET & (1 << 3))); /* wait interrupt */
if (LPC_I2C->STAT != 0x28) {
i2c_stop();
return false;
} else
return true;
}

bool i2c_recv_dat(unsigned char *dat, bool ack)
{
if (!ack)
LPC_I2C->CONCLR = 1 << 2;
else
LPC_I2C->CONSET = 1 << 2;
LPC_I2C->CONCLR = 1 << 3;
while (!(LPC_I2C->CONSET & (1 << 3))); /* wait interrupt */
if (LPC_I2C->STAT != 0x50 && LPC_I2C->STAT != 0x58) {
i2c_stop();
return false;
}
*dat = LPC_I2C->DAT;
return true;
}

int i2c_write(unsigned char addr, unsigned char reg, unsigned char *buf, int len)
{
int i;

/* 1. transmit a START condition */
if (!i2c_start())
return -1;

/* 2. send slave address */
if (!i2c_send_sla(addr << 1))
return -1;

/* 3. send register address */
if (!i2c_send_dat(reg))
return -1;

/* 4. send data */
for (i = 0; i < len; i++) {
if (!i2c_send_dat(buf[i]))
return i;
}

/* 5. generate stop */
i2c_stop();

return len;
}

int i2c_read(unsigned char addr, unsigned char reg, unsigned char *buf, int len)
{
int i = len;

/* 1. transmit a START condition */
if (!i2c_start())
return -1;

/* 2. send slave address */
if (!i2c_send_sla(addr << 1))
return -1;

/* 3. send register address */
if (!i2c_send_dat(reg))
return -1;

/* 4. nonstandard */
i2c_stop();

/* 5. transmit a START condition */
if (!i2c_start())
return -1;

/* 5. send slave address */
if (!i2c_send_sla((addr << 1) | 1))
return -1;

/* 6. receive data */
while (i) {
if (i == 1) {
if (!i2c_recv_dat(buf + len - i, false))
return len - i;
} else {
if (!i2c_recv_dat(buf + len - i, true))
return len - i;
}
i--;
}

/* 7. transmit a STOP condition */
i2c_stop();

return len;
}

light.h

1
2
3
4
5
6
7
8
#ifndef __LIGHT_H__
#define __LIGHT_H__

void light_init(void);
unsigned short light_get(void);
void light_set_thr(unsigned short thrhi, unsigned short thrlo);

#endif

light.c

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
#include "i2c.h"

#define ISL29003_ADDR 0x44
#define COMMAND 0
#define CONTROL 1
#define THRHI 2
#define THRLO 3
#define LUXLSB 4
#define LUXMSB 5
#define TMRLSB 6
#define TMRMSB 7

void light_init(void)
{

/*
* 2^16 cycles, Diode1's current to unsigned 16-bit data,
* Integration is internally timed, Normal operation, enable ADC-core
*/
unsigned char cmd = (1 << 7);
/*
* 0 to 64000 lux,
* Interrupt is triggered after 8 integration cycles
*/
unsigned char ctl = (3 << 2) | (2 << 0);

i2c_init();


i2c_write(ISL29003_ADDR, COMMAND, &cmd, 1);
i2c_write(ISL29003_ADDR, CONTROL, &ctl, 1);
}

unsigned short light_get(void)
{
unsigned short lux;

i2c_read(ISL29003_ADDR, LUXLSB, (unsigned char *)&lux, 2);

return lux;
}

void light_set_thr(unsigned short thrhi, unsigned short thrlo)
{
unsigned char buf[2];

buf[0] = thrhi >> 8;
buf[1] = thrlo >> 8;
i2c_write(ISL29003_ADDR, COMMAND, buf, 1);
}

用这个开发板的应该是在参加华清远见的实训或者学校采购了华清远见的开发板。各位好运。

原文链接

SpringCloud-Netflix

SpringCloud-Netflix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.spring cloud Netflix 一站式解决方案!
api网关,zuul组件
Feign --- HttpClinet --- Http 通信方式,同步,阻塞
服务注册发现: Eureka
熔断机制: Hystrix
。。。
2.Apache Dubbo Zookeeper 半自动,需要整合别人的
API:没有,找第三方组件,或自己实现
Dubbo
Zookeeper
没有:借助 Hystrix
Dubbo这个方案并不完善
3.Spring Cloud Alibaba 一站式解决方案! 更简单
新概念:服务网格~ Server Mesh
istio

万变不离其宗
1.API
2.HTTP,RPC
3.注册和发现
4.熔断机制

常见面试题

1.1、什么是微服务?

1.2、 微服务之间是如何独立通讯的?

1.3、 SpringCloud和Dubbo有哪些区别?

1.4、 SpringBoot和SpringCloud,请你谈谈对他们的理解

1.5、 什么是服务熔断?什么是服务降级?

1.6、 微服务的优缺点分别是什么?说一下你在项目开发中遇到的坑

1.7、 你所知道的微服务服务技术栈有哪些?列举一二

1.8、 eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?

……

什么是微服务?

微服务(Microservice Architecture)是近几年流行的一种架构思想,关于它的概念很难一言以蔽之。

究竟什么是微服务呢?我们再次引用ThoughtWorks公司的首席科学家Martin Fowler于2014年提出的一段话:
原文:https://martinfowler.com/articles/microservices.html

汉化:http://www.bdata-cap.com/newsinfo/1713874.html

微服务与微服务架构

微服务

强调的是服务的大小,他关注的是某一个点,是具体解决某一问题/提供落地对应服务的一个服务应用,下一的看,可以看做是IDEA中的一个个微服务工程,或者Moudle

1
2
IDEA 工具里面使用Maven开发的一个独立的小Moudle,它具体是使用springboot开发的一个小模块,专业的事情交给专业的模块来做,一个模块就做着一件事
强调的是一个个的个体,每个个体完成一个具体的任务或者功能!

微服务架构

一种新的结构形式,Martin Fowler,2014提出

微服务架构是一种架构模式,它提倡将单应用程序划分成一组小的服务,服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务于服务间采用轻量级的通信机制互相协作,每个服务都围绕着具体的业务构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言,工具对其进行构建。

微服务优缺点

优点

  • 单一职责原则
  • 每个服务足够内聚,足够小,代码容易理解,这样能聚焦一个指定的业务功能或业务需求;
  • 开发简单,开发效率提高,一个服务可能就是转移的只干一件事
  • 微服务能够被小团队单独开发,这个小团队是2–5人的开发人员组成
  • 微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。
  • 微服务能使用不同的语言开发
  • 易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如jenkins,Hudson,bamboo
  • 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需通过合作才能体现价值
  • 微服务允许你利用融合最新技术
  • 微服务只是业务逻辑的代码,不会和HTML,CSS或其他界面混合
  • 每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一数据库

缺点

  • 开发人员要处理分布式系统的复杂性
  • 多服务运维难度,随着服务的增加,运维的压力也在增大
  • 系统部署依赖
  • 服务间通信成本
  • 数据统一性
  • 系统集成测试
  • 性能监控……

微服务技术栈有哪些?

服务条条目 落地技术
服务开发 SpringBoot,Spring,SpringMVC
服务配置与管理 Netflix公司的Archaius、阿里的Diamond等
服务注册与发现 Eureka、Consul、Zookeeper等
服务调用 Rest、RPC、gRPC
服务熔断器 Hystrix、Envoy等
负载均衡 Ribbon、Nginx等
服务接口调用(客户端调用服务的简化工具) Feign等
消息队列 Kafka、RabbitMQ、ActiveMQ等
服务配置中心管理 SpringCloudConfig、Chef等
服务路由(API网关) Zuul等
服务监控 Zabbix、Nagios、Metrics、Specatator等
全链路追踪 Zipkin、Brave、Dapper等
服务部署 Docker、OpenStack、Kubernetes等
数据流操作开发包 SpringCloud Stream(封装与Redis、Rabbit、Kafka等发送接收消息)
时间消息总线 SpringCloud Bus

为什么选择SpringCloud作为微服务架构

1.选型依据

  • 整体解决方案和框架成熟
  • 社区热度
  • 可维护性
  • 学习曲线

2.当前各大IT公司用的微服务架构有哪些?

  • 阿里:dubbo+HFS
  • 京东:JSF
  • 新浪:Motan
  • 当当网:DubboX
  • ………

3.各微服务框架对比

功能点/服务框架 Netflix/SpringCloud Motan gRPC Thrift Dubbo/DubboX
功能定位 完整的微服务框架 RPC框架,但整合了ZK或Consul,实现集群环境的基本服务注册/发现 RPC框架 RPC框架 服务框架
支持Rest 是,Ribbon支持多种可插拔的序列化选择
支持RPC 是(Hession2)
支持多语言 是(Rest形式)?
负载均衡 是(服务端zuul+客户端Ribbon),zuul-服务,动态路由,云端负载均衡Eureka(针对中间层服务器) 是(客户端) 是(客户端)
配置服务 Netflix Archaius,SpringCloud Config Server集中配置 是(zookeeper提供)
服务调用链监 是(zuul),zuul提供边缘服务,API网关
高可用/容错 是(服务端Hystrix+客户端Ribbon) 是(客户端) 是(客户端)
典型应用案例 Netflix Sina Google Faceboot
社区活跃程度 一般 一般 2017年后重新开始维护,之前终端了5年
学习难度 中断
文档丰富成都 一般 一般 一般
其他 SpringCloudBus为我们的应用程序带来了更多管理端点 支持降级 Netflix内部在开发集成gRPC IDL定义 实践的公司比较多

什么是SpringCloud?

SpringCloud和SpringBoot关系

  • SpringBoot专注于快速方便的开发单个个体微服务。 -Jar
  • SpringCloud是关注全局的服务服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供:配置管理,服务发现,断路器,路由,微代理,事件总线,全局锁,决策竞选,分布式会话等等集成服务。
  • SpringBoot可以离开SpringCloud独立使用,开发项目,但是SpringCloud离不开SpringBoot,属于依赖关系
  • SpringBoot专注于快速、方便的开发单个个体微服务,SpringCloud关注全局的服务治理框架

Dubbo和SpringCloud技术选型

1.分布式+服务治理Dubbo

目前成熟的互联网架构:应用服务化拆分+消息中间件

1621239485812

2.Dubbo和SpringCloud对比

可以看一下社区活跃度

https://github.com/dubbo

https://github.com/spring-cloud

结果:

Dubbo Spring
服务注册中心 Zookeeper SpringCloud Netflix Eureka
服务调用方式 RPC Rest API
服务监控 Dubbo-monitor SpringBoot Admin
断路器 不完善 SpringCloud Netflix Hystrix
服务网关 SpringCloud Netflix Zuul
分布式配置 SpringCloud Config
服务跟踪 SpringCloud Sleuth
消息总线 SpringCloud Bus
数据流 SpringCloud Stream
批量任务 SpringCloud Task

最大区别:SpringCloud抛弃了Dubbo的RPC通信,采用的是基于HTTP的Resst方式。

解决的问题与不一样:Dubbo的定位是一款RPC框架,SpringCloud的目标是微服务架构下的一站式解决方案

SpringCloud能干什么?

  • Distributed/versioned configuration(分布式/版本控制配置)
  • Service registration and discovery(服务注册与发现)
  • Routing(路由)
  • Service-to-service calls(服务到服务的调用)
  • Load balancing(负载均衡配置)
  • Circuit Breakers(段容器)
  • Distributed messaging(分布式消息管理)
  • ……

SpringCloud在哪里下?

官网:https://projects.spring.io/spring-cloud/

1621240904492

1
这些版本名称的命名方式采用了伦敦地铁站的名称,同时根据字母表的顺序来对应版本时间顺序,比如最早的Release版本:Angel,第二个Release版本:Brixton,然后是Camden、Dalston、Edgware,目前最新的是Finchley版本。

参考书:

SpringCloud

依赖

父级依赖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
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
<?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.sec</groupId>
<artifactId>SpringCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<lombok.version>1.16.18</lombok.version>
<log4j.version>1.2.17</log4j.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springCloud的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springboot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--德鲁伊-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--springboot 启动器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--日志测试-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

</project>

注册中心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<!--eureka服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>

服务提供者依赖

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
<dependencies>
<!--actuator 完善监控信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!--我们需要拿到实体类,所以要配置api module-->
<dependency>
<groupId>com.sec</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jetty-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>

消费者依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.sec</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>

配置文件

注册中心application配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001

#Eureka配置
eureka:
instance:
hostname: eureka7001.com
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #如果为false,则表示自己为注册中心
service-url:
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/


服务提供者application配置文件

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
server:
port: 8001
#mybatis的配置
mybatis:
type-aliases-package: com.suyi.springcloud.pojo
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
#spring的配置
spring:
application:
name: springcloud-provider-dept
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf8&setTimezone=UTC
username: root
password: 123456

#eureka的配置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
instance:
instance-id: springcloud-provider-dept8001
info:
app.name: suyi
company: haojiacheng.cn

消费者application配置文件

1
2
3
4
5
6
7
8
9
server:
port: 80

#Eureka配置
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

数据库文件,三个数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
drop database if exists db01; -- db02/db03
create database db03 default character set utf8;

use db01;-- db02/db03

create table dept(
deptno BIGINT not null PRIMARY KEY auto_increment,
dname varchar(60) not null,
db_source varchar(60)
);

INSERT INTO `dept`(dname,db_source) VALUES ('开发部','DATABASE()');
INSERT INTO `dept`(dname,db_source) VALUES ('人事部','DATABASE()');
INSERT INTO `dept`(dname,db_source) VALUES ('财务部','DATABASE()');
INSERT INTO `dept`(dname,db_source) VALUES ('市场部','DATABASE()');
INSERT INTO `dept`(dname,db_source) VALUES ('运维部','DATABASE()');

select * from dept;

Eureka服务注册与与发现

什么是Eureka?

  • Netflix在设计Eureka时,遵循的就是AP原则
  • Eureka时Netflix的一个子模块,也是核心模块之一,Eureka是一个基于REST的服务,用于定位服务,以实现云端中间层服务发现和故障转移,服务注册与发现对于微服务来说是非常重要的,有了服务发现与注册,只需要使用服务的标识,就可以访问到服务,而不需要修改服务调用的配置文件了,功能类似于Dubbo的注册中心,比如Zookeeper;

原理讲解

  • Eureka的基本架构
    • SpringCloud封装了Netflix公司开发的Eureka模块来实现服务注册和发现(对比Zookeeper)
    • Eureka采用了C-S的架构设计,EurekaServer作为服务服务注册功能的服务器,他是服务注册中心
    • 而系统中的其他微服务。使用Eureka的客户端连接到EurekaServer并维持心跳连接。这样系统的维护人员就可以通过EurekaServer来监控系统中各个微服务是否正常运行,SpringCloud的一些其他模块(比如Zuul)就可以通过EurekaServer来发现系统中的其他微服务,并执行相关的逻辑;
    • 和Dubbo架构对比
      1621317410254
      1621317432029
    • Eureka包含两个组件:Eureka Server和Eureka Client。
    • Eureka Server提供服务注册服务,各个节点启动后,会在EurekaServer中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
  • 三大角色
    • Eureka Server:提供服务的注册与发现
    • Service Provider:将自身服务注册到Eureka中,从而使消费方能够找到
    • Service Consumer:服务消费方从Eureka中获取注册服务列表,从而找到消费服务。
  • 盘点目前工程状况
    • 1621317650022

构建步骤

eureka-server

  1. springcloud-eureka-7001模块建立

  2. pom.xml配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <dependencies>
    <!--eureka服务-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <!--热部署-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    </dependency>

    </dependencies>
  3. 配置application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server:
    port: 7001

    #Eureka配置
    eureka:
    instance:
    hostname: localhost
    client:
    register-with-eureka: false #是否向eureka注册中心注册自己
    fetch-registry: false #如果为false,则表示自己为注册中心
    service-url:
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  4. 服务提供者导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--actuator  完善监控信息-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--eureka-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
  5. 开启服务

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableEurekaClient
    public class DeptProvider_8001 {
    public static void main(String[] args) {
    SpringApplication.run(DeptProvider_8001.class,args);
    }

    }
  6. 首先启动服务注册中心,再启动服务提供者,先7001,后8001,然后访问:http://localhost:7001/

    1621322118617

  7. 配置服务提供者名称
    1621322200573

    1
    2
    3
    info:
    app.name: suyi
    company: haojiacheng.cn
  8. 访问1621322286696

  9. 1621322298265

eureka自我保护机制:好死不如赖活着

一句话总结:某时某刻某一个微服务不可用了,eureka不会立刻清理,依旧会对该微服务的信息进行保存!

  • 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90)秒,但是当网络分区故障发生时,微服务与Eureka之间无法正常同行,以上行为可能变得非常危险了–因为微服务本身其实是健康的,此时本不应该注销这个服务,Eureka通过自我保护机制来解决这个问题–当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进行自我保护模式,一旦进入该模式,EurekaServer就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该EurekaServer节点会自动退出自我保护模式。
  • 在自我保护模式中,EurekaServer会保护服务注册表中的信息,不再注销任何服务实例。当它收到的心跳数重新恢复到阈值以上时,该EurekaServer节点就会自动推出自我保护模式,它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例,一句话:好死不如赖活着
  • 综上,自我保护模式是一种应对网络异常的安全保护措施,它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮和稳定
  • 在SpringCloud中,可以使用eureka.server.enable-self-preservation = false 禁用自我保护模式【不推荐关闭自我保护机制】

eureka集群

为了能区分开,首先改了本地的 ==域名映射==

C:\Windows\System32\drivers\etc\hosts

添加三个域名映射

1
2
3
127.0.0.1       eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7003.com

1.创建三个注册中心

  • springcloud-eureka-7001
  • springcloud-eureka-7002
  • springcloud-eureka-7003

2.导入依赖 三个注册中心的依赖是一样的

1
2
3
4
5
6
7
8
9
10
11
<!--eureka服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>

3.创建启动项 三个都是一样的 记得开启==eureka服务==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sec.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer //开启eureka服务
public class eurekaServer_7001 {
public static void main(String[] args) {
SpringApplication.run(eurekaServer_7001.class,args);
}
}

4.配置application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001

#Eureka配置
eureka:
instance:
hostname: eureka7001.com
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #如果为false,则表示自己为注册中心
service-url:
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/


1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7002

#Eureka配置
eureka:
instance:
hostname: eureka7002.com
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #如果为false,则表示自己为注册中心
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/


1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7003

#Eureka配置
eureka:
instance:
hostname: eureka7003.com
client:
register-with-eureka: false #是否向eureka注册中心注册自己
fetch-registry: false #如果为false,则表示自己为注册中心
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/


ACID是什么?

  • A(Atomicity)原子性
  • C(Consistency)一致性
  • I(Isolation)隔离性
  • D(Durability)持久性

CAP是什么?

  • C(Consistency)强一致性
  • A(Availability)可用性
  • P(Partition tolerance)分区容错性

CAP的三进二:CA、AP、CP

CAP理论的核心

  • 一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求
  • 根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类:
    • CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差
    • CP:满足一致性,分区容错性的系统,通常性能不是特别高
    • AP:满足可用性,分区容错性的系统,通常可能对一致性要求低一些

作为服务注册中心,Eureka比Zookeeper好在哪里?

著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)、P(容错性)。

由于分区容错性P在分布式系统中是必须要保证的,因此我们只能在A和C之间进行权衡。

  • Zookeeper保证的是CP
  • Eureka保证的是AP

Zookeeper保证的是CP

​ 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接收服务直接down掉不可用,也就是说,服务注册功能对可用性的要求高于一致性,但是Zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30~120s,且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zk集群失去master节点是较大概率会发生的事件,虽然服务最终能够恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。

Eureka保证的是AP

​ Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  1. Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
  2. Eureka仍然能够接收新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点依然可用)
  3. 当网络稳定时,当前实例新的注册信息就会被同步到其他节点中

==因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个服务瘫痪==

Ribbon是什么?

  • Spring Cloud Ribbon是基于Netflix Ribbon实现的一套==客户端负载均衡的工具==。
  • 简单地说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon的客户端组件提供一系列完整的配置如:连接超时、重试等等。简单地说,就是在配置文件中列出Load Balancer(简称LB,负载均衡)后面所有的机器,Ribbon会自动的帮你基于某种规则(如简单轮询,随机连接等等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法!
  • 面试造飞机,工作拧螺丝

LVS是什么?

​ LVSLinux Virtual Server的简写,意即Linux虚拟服务器**,是一个虚拟的服务器集群系统。本项目在1998年5月由章文嵩博士成立,是中国国内最早出现的自由软件项目之一。

Ribbon能干嘛?

  • LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。
  • 负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
  • 常见的负载均衡软件有Nginx,LVS等等 Apache+Tomcat
  • dubbo、SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义
  • 负载均衡简单分类:
    • 集中式LB
      • 即在服务的消费方和提供方之间是同独立的LB设施,如Nginx,由该设施负责把访问请求通过某种策略转发至服务的提供方!
    • 进程式LB
      • 将LB逻辑集成到消费方,消费方从注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器。
      • ==Ribbon就属于进程内LB==,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址!

测试目录:

1621657587120

MyRuleConfig.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sec.MyRule;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRuleConfig {

@Bean
public SuRandomRule getSuRandomRule(){
return new SuRandomRule();
}
}

SuRandomRule.java

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
package com.sec.MyRule;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
//import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SuRandomRule extends AbstractLoadBalancerRule {

/*
每个服务用5次?
*/

private int total=0;//每个服务调用的次数,当它==5的时候,调用下一个服务
private int currentIndex=0;//当前的服务索引,当total==5的时候,currentIndex+1;

public SuRandomRule() {
}

//@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
Server server = null;

while(server == null) {
if (Thread.interrupted()) {
return null;
}

List<Server> upList = lb.getReachableServers();//获取活着的服务
List<Server> allList = lb.getAllServers();//获取全部服务
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}

/*int index = this.chooseRandomInt(serverCount);//获取随机数
server = (Server)upList.get(index);//根据随机数来拿到服务列表中的一个服务*/

//-============================================================
if(total<5){
server=upList.get(currentIndex);
total++;
}else{
total=0;
currentIndex++;
if(currentIndex>upList.size()-1){
currentIndex=0;
}
server=upList.get(currentIndex);

}

//-============================================================
if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}

server = null;
Thread.yield();
}
}

return server;
}
}

protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}

public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}

public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}

ConfigBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.sec.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ConfigBean {

//配置负载均衡实现RestTemplate
@Bean
@LoadBalanced //Ribbon
//IRule接口下的↓↓↓↓↓↓↓↓↓↓↓
//AvailabilityFilteringRule:会先过滤掉,跳闸,访问故障的服务,对剩下的服务进行轮询~
//RoundRobinRule:轮询
//RandomRule:随机
//WeightedResponseTimeRule:权重
//RetryRule:会先按照轮询的方式获取服务,如果服务失败,则会在指定的时间内进行,重试
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}

启动类:consumer_80.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sec.springcloud;

import com.sec.MyRule.MyRuleConfig;
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;

@SpringBootApplication
@EnableEurekaClient
//在微服务启动的时候就可以去加载我们自定义的Ribbon类
@RibbonClient(name = "springcloud-provider-dept",configuration = MyRuleConfig.class)
public class consumer_80 {
public static void main(String[] args) {
SpringApplication.run(consumer_80.class);
}
}

Feign负载均衡

简介

​ feign是声明式的web Service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。SpringCloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。

只需要创建一个接口,然后添加注解即可!

feign,主要是社区,大家都习惯面向接口编程。这个是很多开发人员的规范。调用微服务访问两种方法

  1. 微服务名字【Ribbon】
  2. 接口和注解【Feign】

Feign能干什么?

  • Feign旨在使编写java Http客户端变得更容易
  • 前面再使用Ribbon+RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上作了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义,==在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(类似于以前Dao接口上标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可。)==即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign集成了Ribbon

  • 利用Ribbon维护了MicroServiceCloud-Dept的服务列表信息,并且通过轮询实现了客户端的负载均衡,而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而且简单的实现了服务调用。

使用接口方式调用服务

  1. 导入依赖 springcloud-api

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependencies>
    <!--feign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-openfeign-core</artifactId>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>
    </dependencies>
  2. 再编写service层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.suyi.springcloud.service;

    import com.suyi.springcloud.pojo.Dept;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;

    import java.util.List;
    @Component
    @FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT")
    public interface DeptClientService {
    @GetMapping("/dept/queryAll")
    public List<Dept> queryAll();

    @GetMapping("/dept/queryById/{id}")
    public Dept queryById(@PathVariable("id") Long id);

    @PostMapping("/dept/addDept/{dept}")
    public boolean add(@PathVariable("dept") Dept dept);
    }

  3. 创建一个Module springcloud-consumer-dept-feign 导入pom依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>com.sec</groupId>
    <artifactId>springcloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    </dependency>
  4. application.yml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 80

    #Eureka配置
    eureka:
    client:
    register-with-eureka: false
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  5. config->ConfigBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.sec.springcloud.config;

    import org.springframework.cloud.client.loadbalancer.LoadBalanced;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.client.RestTemplate;

    @Configuration
    public class ConfigBean {

    //配置负载均衡实现RestTemplate
    @Bean
    @LoadBalanced //Ribbon
    //IRule接口下的↓↓↓↓↓↓↓↓↓↓↓
    //AvailabilityFilteringRule:会先过滤掉,跳闸,访问故障的服务,对剩下的服务进行轮询~
    //RoundRobinRule:轮询
    //RandomRule:随机
    //WeightedResponseTimeRule:权重
    //RetryRule:会先按照轮询的方式获取服务,如果服务失败,则会在指定的时间内进行,重试
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }

  6. controller->DeptConsumerController

    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
    package com.sec.springcloud.controller;

    import com.suyi.springcloud.pojo.Dept;
    import com.suyi.springcloud.service.DeptClientService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.List;

    @RestController
    public class DeptConsumerController {

    @Autowired
    private DeptClientService deptClientService;

    @RequestMapping("/consumer/dept/add/{dept}")
    public boolean add(@PathVariable("dept") Dept dept){
    return deptClientService.add(dept);
    }

    @RequestMapping("/consumer/dept/queryById/{id}")
    public Dept queryById(@PathVariable("id") Long id){
    return deptClientService.queryById(id);
    }

    @RequestMapping("/consumer/dept/queryAll")
    public List<Dept> queryAll(){
    return deptClientService.queryAll();
    }

    }

  7. 启动项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.sec.springcloud;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;

    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients(basePackages = {"com.suyi.springcloud"})
    public class Feign_consumer_80 {
    public static void main(String[] args) {
    SpringApplication.run(Feign_consumer_80.class);
    }
    }

分布式系统面临的问题

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败!

服务雪崩

​ 多个服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”、如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。

​ 对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

​ 我们需要 弃车保帅

什么是Hystrix

​ Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

​ “断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个服务预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就可以保证了服务调用方的线程不会被长时间,不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩

Hystrix能干嘛

  • 服务降级
  • 服务熔断
  • 服务限流
  • 接近实时的监控
  • …………

官网资料

https://github.com/Netflix/Hystrix/wiki

服务熔断

是什么

​ 熔断机制是对应雪崩效应的一种微服务链路保护机制

​ 当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,==进而熔断该节点微服务的调用,快速返回错误的响应信息==。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand。

实现步骤

  1. 创建springcloud-provider-dept-hystrix-8001

    1621947999245

  2. 导入依赖

    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
    <dependencies>
    <!--hystrix-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <!--actuator 完善监控信息-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--eureka-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <!--我们需要拿到实体类,所以要配置api module-->
    <dependency>
    <groupId>com.sec</groupId>
    <artifactId>springcloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    </dependency>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <!--test-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--jetty-->
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jetty -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>

    <!--热部署-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    </dependency>
    </dependencies>
  3. controller层写一个方法的备选方法

    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
    package com.sec.springcloud.controller;

    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import com.sec.springcloud.service.deptService;
    import com.suyi.springcloud.pojo.Dept;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;

    //提供Restful服务
    @RestController
    public class deptController {

    @Autowired
    deptService ds;

    @GetMapping("/dept/queryById/{id}")
    @HystrixCommand(fallbackMethod = "queryByIdHystrix")
    public Dept queryById(@PathVariable("id") Long id){
    Dept dept = ds.queryById(id);
    if(dept==null){
    throw new RuntimeException("id异常");
    }
    return dept;
    }

    //备选方法
    public Dept queryByIdHystrix(@PathVariable("id") Long id){
    return new Dept().setDeptno(id).setDname("没有该"+id+"的相关信心").setDb_source("未找到该"+id+"的database");

    }
    }

  4. 主启动类上开启断路器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.sec.springcloud;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

    @SpringBootApplication
    @EnableEurekaClient //在服务启动后自动注册到eureka中
    @EnableDiscoveryClient //服务发现
    @EnableCircuitBreaker //启用断路器
    public class DeptProviderHystrix_8001 {
    public static void main(String[] args) {
    SpringApplication.run(DeptProviderHystrix_8001.class,args);
    }

    }

  5. 测试

服务正常被注册到注册中心里来

1621948161573

访问id为1的

1621948201460

访问id不存在的

1621948221526

显示服务IP地址

1
2
3
4
5
6
7
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
instance:
instance-id: springcloud-provider-dept-hystrix-8001
prefer-ip-address: true #true标识显示该服务的ip地址

1621948263976

服务降级

springcloud-api的service下实现DeptClientService接口的实现类

接口:

  • 注意加上==@Component==注解,和

  • fallbackFactory = DeptClientServiceFallbackFactory.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.suyi.springcloud.service;

import com.suyi.springcloud.pojo.Dept;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Component
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT",fallbackFactory = DeptClientServiceFallbackFactory.class)
public interface DeptClientService {
@GetMapping("/dept/queryAll")
public List<Dept> queryAll();

@GetMapping("/dept/queryById/{id}")
public Dept queryById(@PathVariable("id") Long id);

@PostMapping("/dept/addDept/{dept}")
public boolean add(@PathVariable("dept") Dept dept);
}

实现类: FallbackFactory<DeptClientService>

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
package com.suyi.springcloud.service;

import com.suyi.springcloud.pojo.Dept;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.List;
@Component
public class DeptClientServiceFallbackFactory implements FallbackFactory<DeptClientService> {
@Override
public DeptClientService create(Throwable throwable) {
return new DeptClientService() {
@Override
public List<Dept> queryAll() {
return null;
}

@Override
public Dept queryById(Long id) {
return new Dept()
.setDeptno(id)
.setDname("没有对应的信息,客户端提供了降级的信息,这个服务已经被关闭")
.setDb_source("没有数据");
}
@Override
public boolean add(Dept dept) {
return false;
}
};
}
}

springcloud-consumer-dept-feign的启动类上添加注解

注意:

  • @ComponentScans({@ComponentScan(“com.suyi.springcloud”)})
  • ~ com.suyi.springcloud就是springcloud-api的接口的包

开启服务降级 application.yaml

1
2
3
4
#开启服务降级
feign:
hystrix:
enabled: true

启动springcloud-eureka-7001springcloud-provider-dept-8001springcloud-consumer-dept-feign

访问http://localhost/consumer/dept/queryById/1

1622032910108

关掉springcloud-provider-dept-8001

再次访问

1622032967774

总结:

  • 服务熔断:服务端,某个服务超时或异常,引起熔断 类似于保险丝
  • 服务降级:客户端~,从整个网站请求负载考虑,当某个服务熔断或者关闭后,服务将不再被调用
    此时在客户端,我们可以准备一个FallbackFactory,返回一个默认的值(缺省值),整体的服务水平下降了

Hystrix:Dashboard监控流

创建springcloud-consumer-hystrix-dashboard

写启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sec.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard //开启监控页面
public class DeptConsumerDashboard_9001 {
public static void main(String[] args) {
SpringApplication.run(DeptConsumerDashboard_9001.class,args);
}
}

测试访问http://localhost:9081/hystrix

1622036341259

  1. 导入依赖

    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
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>com.sec</groupId>
    <artifactId>springcloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    </dependency>
    </dependencies>
  2. 编写application.yaml

    1
    2
    3
    4
    5
    server:
    port: 9081
    eureka:
    client:
    register-with-eureka: false
  3. springcloud-provider-dept-hystrix-8001的启动类中编写

    1
    2
    3
    4
    5
    6
    7
    //增加一个  Servlet
    @Bean
    public ServletRegistrationBean HystrixMetricsStreamServlet(){
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
    registrationBean.addUrlMappings("/actuator/hystrix.stream");
    return registrationBean;
    }
  4. 访问http://localhost:9081/hystrix.stream

1622036017022

SpringBoot快速入门

SpringBoot:快速入门

什么是Spring

​ spring是一个开源框架,2003年兴起的一个轻量级的Java开发框架,作者:Rod Johnson。

Spring是为了解决企业级应用开发的复杂性而创建的,简化开发

Spring是如何简化java开发的

​ 为了降低Java开发的复杂性,Spring采用了以下四种关键策略:

  1. 基于pojo的轻量级和最小侵入性编程;
  2. 通过IOC(控制反转),依赖注入(DI)和面向接口实现松耦合;
  3. 基于切面(AOP)和惯例进行声明式编程;
  4. 通过切面和模板减少样式代码;

什么是SpringBoot

​ SpringBoot就是一个javaweb的开发框架,和springmvc类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置,”you can *just run”,能迅速的开发web应用,几行代码开发一个http接口.

SpringBoot主要优点

  • 为所有spring开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化web项目
  • 没有冗余代码生成和xml配置的要求

Hello World

创建springboot项目 next

1620206756685

搜索web 勾选springweb 这将在pom.xml自动导入web环境的依赖

1619397305635

创建好之后在SpringbootApplication同级目录下创建controller包,如果不在此创建,则扫描不到包,虽然运行正常,但是显示不出来。

1619397376738

在controller包下写一个简单的方法进行测试

1619397487204

最后在SpringbootApplication中启动

1619397523877

pom.xml

  • spring-boot-dependencies:核心依赖在父工程中!
  • 我们在写或者引入一些springboot依赖的时候,不需要指定版本

启动器

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
  • springboot的启动场景
  • 自动导入web环境所有的依赖
  • springboot会将所有的功能场景,变成一个个的启动器

原理初探

主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.suyi;

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

//标注这个类是一个springboot的应用
@SpringBootApplication
public class Springboot01Application {

public static void main(String[] args) {
//将springboot应用启动
SpringApplication.run(Springboot01Application.class, args);
}

}
  • 注解

    • @SpringBootConfiguration:  springboot的配置
          @Configuration:  spring配置类
          @Component:  spring的组件
      @EnableAutoConfiguration:  自动配置
          @AutoConfigurationPackage:  自动配置包
          @Import(AutoConfigurationPackages.Registrar.class):  自动配置‘包注册’
          @Import(AutoConfigurationImportSelector.class):  自动配置导入选择
      //获取所有的配置
      List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
      
      
      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

      获取候选的配置

      ```java
      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
      List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
      getBeanClassLoader());
      Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
      + "are using a custom packaging, make sure that file is correct.");
      return configurations;
      }
      ```

      META-INF/spring.factories : 自动配置的核心文件![1620206978295](../assets/1620206978295.png)

      结论:springboot所有自动装配都是在启动的时候扫描并加载:`‘spring.factories’`,所有自动配置类都在这里面,但是不一定都生效,因为要导入对应的start,导入start后就有启动器了,然后自动装配才会生效,最后配置成功!

      1. springboot在启动的时候,从类路径下`META-INF/spring.factories`获取指定的值
      2. 将这些自动配置的类导入容器,自动配置就会生效,进行自动配置
      3. 以前需要我们手动配置的东西,现在springboot帮我们自动配置好了
      4. 整合javaEE ,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.2.0.RELEASE.jar 这个包下
      5. 他会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器
      6. 容器中也会存在非常多的xxxxAutoConfigurration的文件,就是这些类给容器中导入了这个场景需要的所有组件,并自动配置,@Configuration,JavaConfig!
      7. 有了自动配置的类,免去了我们手动编写配置文件的工作!

      JavaConfig @Configuration @Bean

      Docker:进程



      关于springboot,谈谈你的理解:

      - 自动配置
      - run()



      全面接管SpringMVC的配置!

      ### SpringBoot 配置

      配置文件:Springboot使用一个全局的配置文件,配置文件名称是固定的

      - application.properties
      - 语法结构:key=value
      - application.yaml
      - 语法结构:key:空格 value

      配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层给我们都已经配置好了。

      #### yaml

      YAML 是 "YAML Ain't a Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。

      YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。

      YAML 的配置文件后缀为 .yml,如:**runoob.yml** 。



      springboot推荐使用yaml,如果不使用yaml,也可以使用properties配置文件。但是需要设置文件的编码格式

      如:![1619409764613](../assets/1620207084686.png)

      注解解释:

      ```java
      /*
      * @ConfigurationProperties的作用:
      * 将配置文件中配置的每一个属性的值,映射到这个组件中;
      * 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
      * 参数 prefix = "person" : 将配置文件中的person下面的所有属性一一对应
      *
      * 只有这个组件时容器中的组件,才能使用容器提供的@ConfigurationProperties功能
      *
      * */
      @ConfigurationProperties(prefix = "person")

      @PropertySource(value = "classpath:suyi.properties")// 加载指定的配置文件 与@Value搭配使用

      ```

      | | @ConfigurationProperties | @Value |
      | :------------------: | :----------------------: | :--------: |
      | 功能 | 批量注入配置文件中的属性 | 一个个指定 |
      | 松散绑定(松散语法) | 支持 | 不支持 |
      | SpEL | 不支持 | 支持 |
      | JSR303数据校验 | 支持 | 不支持 |
      | 复杂类型封装 | 支持 | 不支持 |

      yaml给实体类赋值

      ~~~java
      package com.suyi.pojo;


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

      import java.util.Date;
      import java.util.List;
      import java.util.Map;
      /*
      * @ConfigurationProperties的作用:
      * 将配置文件中配置的每一个属性的值,映射到这个组件中;
      * 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
      * 参数 prefix = "person" : 将配置文件中的person下面的所有属性一一对应
      *
      * 只有这个组件时容器中的组件,才能使用容器提供的@ConfigurationProperties功能
      *
      * */
      @Component //注册bean
      //@PropertySource(value = "classpath:suyi.properties")// 加载指定的配置文件
      @ConfigurationProperties(prefix = "person")
      public class Person {
      //@Value("${name}")
      private String name;
      //@Value("${age}")
      private Integer age;
      private Boolean happy;
      private Date birth;
      private Map<String,Object> maps;
      private List<Object> lists;
      private Dog dog;
      ………………
      }

yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
person:
name: hjc${random.uuid}
age: ${random.int}
happy: false
birth: 2000/11/19
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
hello: 'person的'
dog:
name: ${person.hello}旺财
age: 3

松散绑定

属性松散绑定
表示驼峰式、下划线(_)、短横线(-)

标准方式

person.firstName

方式一

大写用-
person.first-name

方式二

大写用_
person.first_name

三种方式,都可以使用
推荐,属性书写方式
PERSON_FIRST_NAME

JSR303数据校验

springboot中可以用@Validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的email只能支持Email格式。

Student.java

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
package com.suyi.pojo;

import lombok.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Email;

@AllArgsConstructor
@NoArgsConstructor
@ToString
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "student")
@Validated //数据校验
public class Student {
private String firstName;
private Integer age;
//可以自定义提示信息
@Email(message = "邮箱格式错误")
private String email;

}

自定义设置提示错误信息

1619422943844

application.yaml

1
2
3
4
student:
first-name: 郝嘉诚
age: 21
email: 990784805@qq.com #使用正确格式测试一次和使用错误格式测试一次

email错误格式运行结果:

1619422681231

JSR303常用注解

注解 功能
@Null 对象必须为null
@NotNull 对象必须不为null,无法检查长度为0的字符串
@NotBlank 字符串必须不为Null,且去掉前后空格长度必须大于0
@AssertTrue 对象必须为true
@AssertFalse 对象必须为false
@Max(Value) 必须为数字,且小于或等于Value
@Min(Value) 必须为数字,且大于或等于Value
@DecimalMax(Value) 必须为数字( BigDecimal ),且小于或等于Value。小数存在精度
@DecimalMin(Value) 必须为数字( BigDecimal ),且大于或等于Value。小数存在精度
@Digits(integer,fraction) 必须为数字( BigDecimal ),integer整数精度,fraction小数精度
@Size(min,max) 对象(Array、Collection、Map、String)长度必须在给定范围
@Email 字符串必须是合法邮件地址
@Past Date和Calendar对象必须在当前时间之前
@Future Date和Calendar对象必须在当前时间之后
@Pattern(regexp=“正则”) String对象必须符合正则表达式

多环境配置及配置文件位置

配置文件位置的优先级:

1620207822634

多环境配置:(dev,test,默认)

方法一:多配置文件

  • 默认 application.properties:
1
2
#spring的多环境配置,可以选择激活哪一个配置!
spring.profiles.active=test
  • dev application-dev.properties:
1
server.port=8081
  • test application-test.properties:
1
server.port=8082

方法二:yml的多文档块

和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8081
spring:
profiles:
active: test
---
server:
port: 8082
spring:
profiles: dev
---
server:
port: 8083
spring:
profiles: test

SpringBoot Web开发

jar:webapp!

自动装配

springboot到底帮我们配置了什么?我们能不能进行修改?能修改哪些东西?能不能扩展?

  • xxxAutoConfiguration..向容器中自动配置组件
  • xxxProperties:自动配置类,装配配置文件中自定义的一些内容!

要解决的问题:

  • 导入静态资源……
  • 首页
  • jsp,模板引擎Thymeleaf
  • 装配扩展SpringMVC
  • 增删改查
  • 拦截器
  • 国际化!

静态资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}

什么是webjars?

​ WebJars是将客户端(浏览器)资源(JavaScript,Css等)打成jar包文件,以对资源进行统一依赖管理。WebJars的jar包部署在Maven中央仓库上。

为什么使用

​ 我们在开发Java web项目的时候会使用像Maven,Gradle等构建工具以实现对jar包版本依赖管理,以及项目的自动化管理,但是对于JavaScript,Css等前端资源包,我们只能采用拷贝到webapp目录下的手工方式,这样做就无法对这些资源进行依赖管理。而且容易导致文件混乱、版本不一致等问题。那么WebJars就提供给我们这些前端资源的jar包形式,我们就可以进行依赖管理

​ WebJars是将这些通用的Web前端资源打包成Java的Jar包,然后借助Maven工具对其管理,保证这些Web资源版本唯一性,升级也比较容易。关于webjars资源,有一个专门的网站http://www.webjars.org/,我们可以到这个**网站上找到自己需要的资源,在自己的工程中添加入maven依赖,即可直接使用这些资源了**。

总结:

  1. 在springboot,我们可以使用以下方式处理静态资源
    • webjars http://localhost:8080/webjars/jquery/3.5.1/jquery.js
    • public ,statis,/**,resources localhost:8080/1.js
  2. 优先级:resource>static(默认)>public

首页:

​ 直接在public或resource或static文件夹下编写html,推荐在public中

1619489820346

页面图标制定:

​ 在public或static下创建favicon.ico的图片

1619491964832

然后在application.properties中写入

1
spring.mvc.favicon.enabled=false

模板引擎:

导入包

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

要在templates文件夹中创建html页面,否则识别不到

这里要注意的是在页面头部一定要引入 xmlns:th="http://www.thymeleaf.org"

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="${msg}"></div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sec.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloController {
@RequestMapping("/test")
public String test(Model model){
model.addAttribute("msg","hello,springboot!!!");
return "test";
}
}

访问:localhost:8080/test

Thymeleaf语法:

· 简单表达式 (simple expressions)

  •   ${…} 变量表达式
  •   *{…} 选择变量表达式
  •   #{…} 消息表达式
  •   @{…} 链接url表达式

· 字面量

  •   ‘one text’,’another one!’,… 文本
  •   0,34,3.0,12.3,… 数值
  •   true false 布尔类型
  •   null 空
  • ​ one,sometext,main 文本字符

· 文本操作

  •   + 字符串连接
  •   |The name is ${name}| 字符串连接

· 算术运算

  •   + , - , * , / , % 二元运算符
  •   - 负号(一元运算符)

· 布尔操作

  •   and,or 二元操作符
  •   !,not 非(一元操作符)

· 关系操作符

  •   > , < , >= , <= (gt , lt , ge , le)
  •   == , != (eq, ne)

· 条件判断

  • (if) ? (then) if-then
  • (if) ? (then) : (else) if-then-else

详细语法:https://www.codercto.com/a/50477.html

MVC配置原理:

视图解析器:

ContentNegotiatingViewResolver.java——ViewResolver(接口)

ViewResolver 实现了视图解析器接口的类,我们就可以把它看做视图解析器

MyMvcConfig.java

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
package com.sec.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Locale;

//扩展springmvc
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//public interface ViewResolver 实现了视图解析器接口的类,我们就可以把它看做试图解析器
@Bean
public ViewResolver MyViewResolver(){
return new MyViewResolver();
}

//自定义了一个自己的视图解析器MyViewResolver
public static class MyViewResolver implements ViewResolver{

@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
}

视图跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.sec.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Locale;

//扩展springmvc 官方建议这样做
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {


//视图跳转
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/qwe").setViewName("test");
}

}

在springboot中,有非常多的xxxxConfiguration,帮助我们进行扩展配置,只要看到了这个东西,我们就要注意了

国际化:

  1. 首页配置:
    1. 注意点,所有页面的静态资源都需要使用thymeleaf接管;@{}
    2. url: @{}
  2. 页面国际化:
    1. 我们需要配置i18n文件
    2. 我们如果需要在项目中进行按钮自动切换,我们需要自定义一个组件LocaleResolver
    3. 记得将自己写的组件配置到spring容器 @Bean
    4. #{}

实现步骤:

1619516834028

② MyLocaleResolver.java:

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
package com.sec.config;

import org.springframework.web.servlet.LocaleResolver;
import org.thymeleaf.util.StringUtils;

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

public class MyLocaleResolver implements LocaleResolver {


@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中的参数
String l = request.getParameter("l");
//如果没有就使用默认的
Locale locale = Locale.getDefault();
//如果请求的链接携带了国际化的参数
if(!StringUtils.isEmpty(l)){
//zh_CN
String[] split = l.split("_");
//国家,地区
locale = new Locale(split[0], split[1]);
}
return locale;
}

@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

}
}

1619516629377

拦截器:

①登录跳转时设置session

1619527437852

②在config文件下创建————–LoginHandlerInterceptor.java

1619527505539

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.sec.config;

import org.springframework.web.servlet.HandlerInterceptor;

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

public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("user");
if(user==null){
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else{
return true;
}
}
}

③在自定义的配置文件中写以下内容:

1619527614742

代码:

1
2
3
4
5
//拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html","/","/css/**","/js/**","/img/**","/user/login");
}

自定义错误页面:

1619527729539

员工列表展示:

  1. 提取公共页面
    1. 把公共部分写到commons文件夹下 命名为commons.html1619573100721
    2. 使用th:replace=”~{}”1619573150004
  2. 列表循环展示

添加员工:

  1. <!--按钮跳转-->
    <h2><a href="" class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    2. 跳转至添加员工的页面

    ~~~java
    @GetMapping("/emp")
    public String toAddPage(Model model){
    //获取部门
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);
    return "emp/add";
    }
  2. 添加员工的页面1619596902531

  3. controller执行添加

1
2
3
4
5
6
@PostMapping("/emp")
public String addEmp(Employee employee){
//添加的操作
employeeDao.save(employee);
return "redirect:emps";//返回首页
}

查询:

  1. 使用thymeleaf语法跳转controller,利用controller实现查询方法1619597353071

  2. @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "emp/list";
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    3. ![1619597410911](../assets/1619597410911.png)



    #### 修改:

    1. ![1619597453919](../assets/1619597453919.png)

    2. 跳转修改页面

    ~~~java
    @GetMapping("/emp/{id}")
    public String updEmp(@PathVariable("id")Integer id,Model model){
    //查出原来的数据
    Employee emlpoyee = employeeDao.getEmlpoyeeById(id);
    model.addAttribute("emp",emlpoyee);
    //查询部门信息
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);
    return "emp/update";
    }
  3. 修改页面

    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
    <div th:replace="~{commons/commons::topbar}"></div>

    <div class="container-fluid">
    <div class="row">
    <div th:replace="~{commons/commons::sidebar}"></div>

    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
    <div class="form-group">
    <lable>姓名</lable>
    <input type="hidden" class="form-control" name="id" th:value="${emp.getId()}">
    <input type="text" class="form-control" name="lastName" th:value="${emp.getLastName()}" />
    </div>
    <div class="form-group">
    <lable>邮箱</lable>
    <input type="email" class="form-control" name="email" th:value="${emp.getEmail()}" />
    </div>
    <div class="form-group">
    <lable>性别</lable>
    <br>
    <div class="form-check form-check-inline">
    <input class=form-check-input type="radio" name="gender" value="0" th:checked="${emp.getGender()==0}" />
    <lable class=form-check-label></lable>
    </div>
    <div class="form-check form-check-inline">
    <input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp.getGender()==1}" />
    <lable class="form-check-label"></lable>
    </div>
    </div>
    <div class="form-group">
    <label>部门</label>
    <select class="form-control" name="department.id">
    <option th:selected="${dep.getDepartmentName()==emp.getDepartment()}" th:each="dep:${departments}" th:text="${dep.getDepartmentName()}" th:value="${dep.getId()}"></option>
    </select>
    </div>
    <div class="form-group">
    <label>生日</label>
    <input type="text" class="form-control" name="birth" placeholder="2020-5-22" th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" />
    </div>
    <button type="submit" class="btn btn-primary">修改</button>
    </form>

    </main>
    </div>
    </div>
  4. 执行修改

    1
    2
    3
    4
    5
    @PostMapping("/updateEmp")
    public String doUpdEmp(Employee employee){
    employeeDao.save(employee);
    return "redirect:/emps";
    }

    删除:

    1. 按钮1619597763795

    2. 执行删除

      1
      2
      3
      4
      5
      @GetMapping("/delEmp/{id}")
      public String delEmp(@PathVariable("id")Integer id){
      employeeDao.delete(id);
      return "redirect:/emps";
      }

      404页面:

      在templates文件下创建error文件夹,存放404页面,springboot自定识别

      1619597883852

      访问一个错误的页面时,会跳转至404页面;

      500,400页面都是如此!

前端:

  • 模板:别人写好的,我们拿来改就行了

  • 框架:组件:自己手动组合拼接!Bootstrap,Layui,semantic-ui

    • 栅格系统
    • 导航栏
    • 侧边栏
    • 表单
  • 前端搞定后,知道页面长什么样子,数据怎么传

  • 设计数据库(难点!)

  • 前端可以独立运行,独立化工程

  • 数据接口如何对接:json,对象all in one!

  • 前后端联调测试!

  1. 有一套自己熟悉的后台模板:工作必要!,———-xadmin

  2. 前端界面:至少自己能够通过前端框架,组合出来一个网站页面

    1. index
    2. about
    3. blog
    4. post
    5. user
  3. 让这个网站可以独立运行!

  • JDBC
  • mybatis:重点
  • Druid:重点
  • Shiro:重点 (安全)
  • Spring Security:重点 (安全)
  • 异步任务,邮件发送,定时任务
  • Swagger
  • Dubbo+Zookeeper

整合druid数据源

  1. application.yml

    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
    #spring:
    # datasource:
    # username: root
    # password: 123456
    # rul: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    # driver: com.mysql.jdbc.Driver

    spring:
    datasource:
    data:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource


    #springboot默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计,log4j:日志记录、wall:防御sql注入
    #如果允许时报错 java.lang.ClassNotFoundException:org.apache.log4j.Priority
    #则导入 log4j 依赖即可,maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMills=500
  2. config===>DruidConfig.java

    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
    package com.sec.config;

    import com.alibaba.druid.pool.DruidDataSource;
    import com.alibaba.druid.support.http.StatViewServlet;
    import com.alibaba.druid.support.http.WebStatFilter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.boot.web.servlet.ServletRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import javax.servlet.Filter;
    import javax.sql.DataSource;
    import java.util.HashMap;

    @Configuration
    public class DruidConfig {
    //与application.yml绑定
    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource druidDataSource(){
    return new DruidDataSource();
    }

    //后台监控 :web.xml , ServletRegistrationBean
    //因为springboot内置了servlet容器,所以没有web.xml, 替方法:ServletRegistrationBean
    @Bean
    public ServletRegistrationBean statViewServlet(){
    ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    HashMap<String, String> initParameters = new HashMap<>();
    initParameters.put("loginUsername","admin");
    initParameters.put("loginPassword","123456");

    //谁可以访问
    initParameters.put("allow","");

    //谁不能访问 initParameters.put("haojiacheng","192.168.11.123");

    bean.setInitParameters(initParameters);//设置初始化参数

    return bean;
    }
    //filter过滤器
    @Bean
    public FilterRegistrationBean webStatFilter(){
    FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
    bean.setFilter(new WebStatFilter());
    //可以过滤哪些请求呢?
    HashMap<String, String> initParameters = new HashMap<>();
    //这些东西不进行统计
    initParameters.put("exclusions","*.js,*.css,/druid/*");
    bean.setInitParameters(initParameters);
    return bean;
    }
    }

整合mybatis数据源

  1. 导入包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>
  2. 配置文件
    application.properties

    1
    2
    3
    4
    5
    6
    7
    8
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8

    #整合mybatis
    mybatis.type-aliases-package=com.sec.pojo
    mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
  3. 编写sql

  4. 业务层调用dao层

springSecurity(安全)

在web开发中,安全第一位!过滤器,拦截器

功能性需求:否

做网站:安全应该在设计之初的时候考虑

  • 漏洞,隐私泄露
  • 架构一旦确定,更改成本很高

shiro、springSecurity:很像、除了类不一样,名字不一样;

认证,授权(vip1,vip2,vip3)

  • 功能权限
  • 访问权限
  • 菜单权限
  • ……拦截器,过滤器:大量的原生代码 冗余

MVC—spring—springboot—架构思想

AOP:横切—配置类

简介

Spring Security是针对spring项目的安全框架,也是springboot底层安全模块默认的技术选型,他可以实现强大的web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式 @Enablexxxx开启某个功能

spring security的两个主要目标是“认证”和“授权”(访问控制)。

“认证”(Authentication)

“授权”(Authorization)

这个概念是通用的,而不是只在springsecurity中存在。

参考官网:https://spring.io/projects/spring-security ,查看我们自己项目中的版本,找到对应的帮助文档:

https://docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/htmlsingle

环境搭建:

在com.sec下创建config,在config下创建SecurityConfig

导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--security-thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

授权

  • 权限访问问题
  • 防止网站攻击,也是注销失败可能出现的原因

认证

  • 通过设置用户登录名和密码以及权限来相对于的显示页面当前权限该显示的模块
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
package com.sec.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

//AOP思想
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//授权
//链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,功能页只有有权限的人才可以访问
//请求权限的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限默认跳转到登录界面 /login
http.formLogin();


//防止网站攻击
http.csrf().disable();//注销失败可能出现的原因

//注销 清楚cookie和session: logout().deleteCookies("remove").invalidateHttpSession(true)
// .logoutUrl() :注销的页面 logoutSuccessUrl("/") : 注销成功的页面
http.logout().logoutSuccessUrl("/");

}

//认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("haojiacheng").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and().withUser("mihua").password(new BCryptPasswordEncoder().encode("654321")).roles("vip1","vip2","vip3")
.and().withUser("user").password(new BCryptPasswordEncoder().encode("111111")).roles("vip1")
.and().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and().withUser("lisi").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2")
.and().withUser("wangwu").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");

}
}

用户名与角色权限显示

1619700092953

不同的权限显示不同的模块:

1619699458129

记住我:

1619700528150

  • 用户勾选后,网页存入cookie,cookie存在时长14天
  • 但是用户手动删除cookie或者注销,则记住我失效

首页定制:

  • loginPage(“/toLogin”):设置自定义登录页面

  • usernameParameter(“user”):设置自定义登录页面的用户输入的用户名的name

  • passwordParameter(“pwd”):设置自定义登录页面的用户输入的密码的name

    • <form th:action="@{/login}" method="post">
          <div class="field">
              <label>Username</label>
              <div class="ui left icon input">
                  <input type="text" placeholder="Username" name="user">
                  <i class="user icon"></i>
              </div>
          </div>
          <div class="field">
              <label>Password</label>
              <div class="ui left icon input">
                  <input type="password" name="pwd">
                  <i class="lock icon"></i>
              </div>
          </div>
          <div class="field">
              <input type="checkbox" name="remember">记住我
          </div>
          <input type="submit" class="ui blue submit button"/>
      </form>
      
      1
      2
      3
      4
      5

      - loginProcessingUrl("/login"):设置登录提交信息的页面,也就是自己跳转自己,springboot判断正确则跳至主页面

      ~~~java
      http.formLogin().loginPage("/toLogin").usernameParameter("user").passwordParameter("pwd").loginProcessingUrl("/login");

Shiro

maven项目:

  1. 导入依赖 pom.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <dependencies>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
    </dependency>

    <!-- configure logging -->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.21</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
    </dependency>
    <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
    </dependency>
    </dependencies>
  2. 配置文件

    1. log4j.properties:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      log4j.rootLogger=INFO, stdout

      log4j.appender.stdout=org.apache.log4j.ConsoleAppender
      log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
      log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

      # General Apache libraries
      log4j.logger.org.apache=WARN

      # Spring
      log4j.logger.org.springframework=WARN

      # Default Shiro logging
      log4j.logger.org.apache.shiro=INFO

      # Disable verbose logging
      log4j.logger.org.apache.shiro.util.ThreadContext=WARN
      log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
    2. shiro.ini: 如果没有代码没有高亮 去下载idea的插件:ini

      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
      [users]
      # user 'root' with password 'secret' and the 'admin' role
      root = secret, admin
      # user 'guest' with the password 'guest' and the 'guest' role
      guest = guest, guest
      # user 'presidentskroob' with password '12345' ("That's the same combination on
      # my luggage!!!" ;)), and role 'president'
      presidentskroob = 12345, president
      # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
      darkhelmet = ludicrousspeed, darklord, schwartz
      # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
      lonestarr = vespa, goodguy, schwartz

      # -----------------------------------------------------------------------------
      # Roles with assigned permissions
      #
      # Each line conforms to the format defined in the
      # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
      # -----------------------------------------------------------------------------
      [roles]
      # 'admin' role has all permissions, indicated by the wildcard '*'
      admin = *
      # The 'schwartz' role can do anything (*) with any lightsaber:
      schwartz = lightsaber:*
      # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
      # license plate 'eagle5' (instance specific id)
      goodguy = winnebago:drive:eagle5
    3. QuickStart.java:

      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
      import org.apache.shiro.SecurityUtils;
      import org.apache.shiro.authc.*;
      import org.apache.shiro.config.IniSecurityManagerFactory;
      import org.apache.shiro.mgt.SecurityManager;
      import org.apache.shiro.session.Session;
      import org.apache.shiro.subject.Subject;
      import org.apache.shiro.util.Factory;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      public class Quickstart {

      private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


      public static void main(String[] args) {

      // 创建具有已配置领域,用户,角色和权限的Shiro SecurityManager的最简单方法是使用简单的INI配置。
      // 我们将通过使用可以提取.ini文件并返回SecurityManager实例的工厂来做到这一点:
      // 在类路径的根目录下使用shiro.ini文件(分别从文件和url加载前缀:file:和url:):
      Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
      SecurityManager securityManager = factory.getInstance();

      // 对于这个简单的示例快速入门,使SecurityManager可以作为JVM单例进行访问。大多数应用程序不会这样做,
      // 而是依赖于其容器配置或webapps的web.xml。这超出了此简单快速入门的范围,因此我们将只做最少的工作,
      // 以便您可以继续感受一下。
      SecurityUtils.setSecurityManager(securityManager);

      // 获取当前执行的用户:Subject
      Subject currentUser = SecurityUtils.getSubject();

      // 通过当前用户拿到session
      Session session = currentUser.getSession();
      session.setAttribute("someKey", "aValue");
      String value = (String) session.getAttribute("someKey");
      if (value.equals("aValue")) {
      log.info("Subject==>Session [" + value + "]");
      }

      // 判断当前的用户是否被认证 isAuthenticated=>已认证
      if (!currentUser.isAuthenticated()) {
      UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
      token.setRememberMe(true);
      try {
      //登录
      currentUser.login(token);
      } catch (UnknownAccountException uae) {//未知帐户异常
      log.info("There is no user with username of " + token.getPrincipal());
      } catch (IncorrectCredentialsException ice) {//账号密码不正确
      log.info("Password for account " + token.getPrincipal() + " was incorrect!");
      } catch (LockedAccountException lae) {//锁定帐户异常
      log.info("The account for username " + token.getPrincipal() + " is locked. " +
      "Please contact your administrator to unlock it.");
      }
      // 在这里捕获更多异常(也许是针对您的应用程序的自定义异常?
      catch (AuthenticationException ae) {
      //意外情况?错误?
      }
      }

      //说出他们是谁:打印其身份验证主体(在这种情况下,为用户名):
      log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

      //测试角色:
      if (currentUser.hasRole("schwartz")) {
      log.info("May the Schwartz be with you!");
      } else {
      log.info("Hello, mere mortal.");
      }

      //测试类型化的权限(不是实例级别) isPermitted: 被允许
      if (currentUser.isPermitted("lightsaber:wield")) {
      log.info("You may use a lightsaber ring. Use it wisely.");
      } else {
      log.info("Sorry, lightsaber rings are for schwartz masters only.");
      }

      //(非常强大的)实例级别权限:
      if (currentUser.isPermitted("winnebago:drive:eagle5")) {
      log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
      "Here are the keys - have fun!");
      } else {
      log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
      }

      //注销!
      currentUser.logout();

      System.exit(0);
      }
      }

  3. Hello World!
    1619745491515

Spring Security都有

1
2
3
4
5
6
7
8
9
Subject currentUser = SecurityUtils.getSubject();//获取当前用户
Session session = currentUser.getSession();//通过当前用户获取session
!currentUser.isAuthenticated();//当前用户未认证
currentUser.login(token);//登录
currentUser.hasRole("schwartz");//当前用户的角色
currentUser.isPermitted("lightsaber:wield");//当前用户有什么权限
currentUser.isPermitted("winnebago:drive:eagle5");//权限
currentUser.logout();//注销
System.exit(0);//系统结束

springboot集成shiro

环境搭建

  • pom.xml: spring-boot-starter-parent===>2.4.5
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
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
</dependency>
</dependencies>
  • 配置文件和controller
    1619750994887

    • ShiroConfig:java:

      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
      package com.sec.config;

      import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
      import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;

      @Configuration
      public class ShiroConfig {
      //ShiroFilterFactoryBean:3
      @Bean
      public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
      ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
      //设置安全管理器
      bean.setSecurityManager(defaultWebSecurityManager);
      return bean;
      }
      //DefaultWebSecurityManager:2
      //如果我们在某个注入点需要另一个 bean,我们需要专门指出它。我们可以通过 @Qualifier 注解来做到这一点。
      @Bean
      public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
      DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      //关联UserRealm
      securityManager.setRealm(userRealm);

      return securityManager;
      }

      //创建 realm 对象 , 需要自定义:1
      @Bean(name = "userRealm")
      public UserRealm userRealm(){
      return new UserRealm();
      }
      }

    • UserRealm.java

      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
      package com.sec.config;


      import org.apache.shiro.authc.AuthenticationException;
      import org.apache.shiro.authc.AuthenticationInfo;
      import org.apache.shiro.authc.AuthenticationToken;
      import org.apache.shiro.authz.AuthorizationInfo;
      import org.apache.shiro.realm.AuthorizingRealm;
      import org.apache.shiro.subject.PrincipalCollection;

      //自定义的 UserRealm
      public class UserRealm extends AuthorizingRealm {
      @Override
      //doGetAuthenticationInfo:获取身份验证信息
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      System.out.println("执行了=>授权doGetAuthenticationInfo:获取身份验证信息");
      return null;
      }

      @Override
      //doGetAuthorizationInfo:获取授权信息
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      System.out.println("执行了=>doGetAuthorizationInfo:获取授权信息");
      return null;
      }
      }

    • MyController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      package com.sec.controller;

      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.web.bind.annotation.RequestMapping;

      @Controller
      public class MyController {
      @RequestMapping({"/","/index","/index.html"})
      public String toIndex(Model model){
      model.addAttribute("msg","hello shiro");
      return "index";
      }
      @RequestMapping("/user/add")
      public String add(){
      return "user/add";
      }

      @RequestMapping("/user/upd")
      public String upd(){
      return "/user/upd";
      }
      }

  • 页面:
    1619751125107

    • add.html:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>Title</title>
      </head>
      <body>
      <h1>add</h1>
      </body>
      </html>
    • upd.html:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>Title</title>
      </head>
      <body>
      <h1>upd</h1>
      </body>
      </html>
    • index.html:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
      <meta charset="UTF-8">
      <title>Title</title>
      </head>
      <body>
      <h2>我是index</h2>
      <p th:text="${msg}"></p>
      <hr>

      <a th:href="@{/user/add}">add</a> || <a th:href="@{/user/upd}">upd</a>
      </body>
      </html>
    • application.properties:

      1
      spring.thymeleaf.cache=false

Shiro实现登录拦截

  • Shiro的内置过滤器

    • anon:无需认证就可以访问
    • authc:必须认证了才能访问
    • user:必须拥有 记住我 功能才能用
    • perms:拥有对某个资源的权限才能访问
  • LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    filterMap.put("/user/*","authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //设置登录的请求
    bean.setLoginUrl("/toLogin");
    
    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







    1. UserRealm:

    ~~~java
    @Override
    //doGetAuthenticationInfo:获取身份验证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo:获取身份验证信息");

    //用户名和密码 原本来数据库中取
    String name="admin";
    String pwd="123123";

    //加密令牌
    UsernamePasswordToken uToken= (UsernamePasswordToken) authenticationToken;
    //判断用户名
    if(!uToken.getUsername().equals(name)){
    return null;//这里返回null 也就是抛出异常 UnknownAccountException
    }
    //密码认证:shiro做
    return new SimpleAuthenticationInfo("",pwd,"");
    }
  1. login.html: 登录界面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>登录</title>
    </head>
    <body>
    <h1>登录</h1>
    <p th:text="${msg}" style="color: red"></p>
    <form th:action="@{/login}" method="post">
    <p>用户名:<input type="text" name="username"></p>

    <p>密码:<input type="password" name="password"></p>
    <p><input type="submit" ></p>
    </form>
    </body>
    </html>
  2. 跳转登录 and 登录执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @RequestMapping("/toLogin")
    public String toLogin(){
    return "login";
    }

    @RequestMapping("/login")
    public String login(String username,String password,Model model){
    //获取当前用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
    subject.login(token);
    return "index";
    } catch (UnknownAccountException e) {
    model.addAttribute("msg","用户名错误");
    return "login";
    } catch (IncorrectCredentialsException e){
    model.addAttribute("msg","密码错误");
    return "login";
    }
    }

Shiro整合mybatis

  1. pom.xml:

    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
    <dependencies>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.3.2</version>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
    </dependency>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
    </dependency>
    <dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <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>
    </dependency>
    </dependencies>
  2. application.properties:

    1
    2
    3
    4
    5
    6
    7
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8

    mybatis.type-aliases-package=com.sec.pojo
    mybatis.mapper-locations=classpath:mapper/*.xml
  3. UserRealm.java

    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
    package com.sec.config;

    import com.sec.pojo.User;
    import com.sec.service.userService;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.springframework.beans.factory.annotation.Autowired;

    //自定义的 UserRealm
    public class UserRealm extends AuthorizingRealm {

    @Autowired
    private userService uS;

    //授权
    @Override
    //doGetAuthorizationInfo:获取授权信息
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=>授权doGetAuthorizationInfo:获取授权信息");
    return null;
    }

    //认证
    @Override
    //doGetAuthenticationInfo:获取身份验证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了=>认证doGetAuthenticationInfo:获取身份验证信息");
    //加密令牌
    UsernamePasswordToken uToken= (UsernamePasswordToken) authenticationToken;
    User u = uS.selectUserByName(uToken.getUsername());
    if(u==null){
    return null;
    }
    //密码可以加密:MD5 MD5盐值加密
    //密码认证:shiro做
    return new SimpleAuthenticationInfo("",u.getPwd(),"");
    }
    }

  4. mapper=>userMapper.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.sec.mapper;

    import com.sec.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;

    @Repository
    @Mapper
    public interface userMapper {
    public User selectUserByName(String name);
    }

  5. service=>userService.java

    1
    2
    3
    4
    5
    6
    7
    8
    package com.sec.service;

    import com.sec.pojo.User;

    public interface userService {
    public User selectUserByName(String name);
    }

  6. service=>userServiceImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.sec.service;

    import com.sec.mapper.userMapper;
    import com.sec.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class userServiceImpl implements userService{

    @Autowired
    private userMapper uM;

    @Override
    public User selectUserByName(String name) {
    return uM.selectUserByName(name);
    }
    }

  7. resource=>mapper=>userMapper.xml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?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.sec.mapper.userMapper">
    <select id="selectUserByName" resultType="User">
    select * from user where name = #{name}
    </select>
    </mapper>
  8. pojo=>User.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.sec.pojo;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.ToString;

    @Data
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
    private int id;
    private String name;
    private String pwd;
    }

请求授权实现

  1. //授权,正常的情况下,没有授权会跳转到未授权页面
    filterMap.put("/user/add","perms[user:add]");//用户user需要拥有对add资源的权限
    
    1
    2
    3
    4

    2. ~~~java
    //设置未授权页面
    bean.setUnauthorizedUrl("/noauth");
  2. @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized(){
        return "未经授权不得进入";
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    ![1620224715645](../assets/1620224715645.png)

    #### 用户登录访问授权

    - 从数据库拿用户的权限:

    ~~~java
    //授权
    @Override
    //doGetAuthorizationInfo:获取授权信息
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=>授权doGetAuthorizationInfo:获取授权信息");
    //SimpleAuthorizationInfo
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //拿到当前登录的这个对象
    Subject subject = SecurityUtils.getSubject();
    //拿到user对象
    User currentUser = (User) subject.getPrincipal();
    //设置当前用户权限
    info.addStringPermission(currentUser.getPerms());
    return info;
    }
  • 设置用户对某资源的权限

    1
    2
    3
    //授权,正常的情况下,没有授权会跳转到未授权页面
    filterMap.put("/user/add","perms[user:add]");
    filterMap.put("/user/upd","perms[user:update]");

数据库表:1620225869814

测试结果:root用户没有进入add的权限,user用户没有进入upd的权限

Shiro整合Thymeleaf

导入依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

写Bean

1
2
3
4
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
  • 方法一:在认证时存入当前用户到session中

    1
    2
    3
    Subject subject = SecurityUtils.getSubject();
    Session session = subject.getSession();
    session.setAttribute("loginUser",u);
    1
    2
    3
    <div th:if="${session.get('loginUser')==null}">
    <a th:href="@{/toLogin}">登录</a>
    </div>
  • 方法二:

    1
    2
    3
    <div shiro:guest="true">
    <a th:href="@{/toLogin}">登录</a>
    </div>
  • 方法三:

    1
    2
    3
    <div shiro:notAuthenticated="">
    <a th:href="@{/toLogin}">登录</a>
    </div>
1
2
3
4
5
6
7
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
<a th:href="@{/user/upd}">upd</a>
</div>

Swagger

学习目标:

  • 了解Swagger的作用和概念
  • 了解前后端分离
  • 在SpringBoot中集成Swagger

Swagger简介

前后端分离

Vue + SpringBoot

后端时代:前端只用管理静态页面; html==》后端。模板引擎 JSP =》 后端是主力

前后端分离时代:

  • 后端:后端控制层、服务层、数据访问层【后端团队】
  • 前端:前端控制层、视图层【前端团队】
    • 伪造后端数据,json。已经存在了,不需要后端,前端工程依旧可以跑起来
  • 前后端如何交互?==》API
  • 前后端相对独立,松耦合;
  • 前后端甚至可以部署在不同的服务器上;

产生一个问题:

  • 前后端集成联调,前端人员和后端人员无法做到“及时协商,尽早解决”,最终导致问题集中爆发;

解决方案:

  • 首先指定schema[计划的提纲],实时更新最新API,降低了集成的风险;
  • 早些年:制定word计划文档;
  • 前后端分离:
    • 前端测试后端接口:postman
    • 后端提供接口,需要实时更新最新的消息及改动!

Swagger

  • 号称世界上最流行的API框架
  • RestFul API 文档在线自动生成工具=》==API文档与API定义同步更新==
  • 直接运行,可以在线测试API接口;
  • 支持多种语言:(java,PHP);

官网:https://swagger.io

在项目使用Swagger需要springbox;

  • swagger2
  • ui

SpringBoot 集成Swagger

  1. 新建一个springboot-web项目

  2. 导入相关依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
    </dependency>

  3. 编写一个Hello工程

  4. 配置swagger==》Config

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.sec.config;

    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;

    @Configuration
    @EnableSwagger2 //开启Swagger2
    public class SwaggerConfig {

    }
  5. 测试运行
    http://localhost:8080/swagger-ui.html

1620292186287

配置Swagger

Swagger的bean实例Docket;

com.sec.config=>SwaggerConfig.java

  • package com.sec.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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;
    
    import java.util.ArrayList;
    
    @Configuration
    @EnableSwagger2     //开启Swagger2
    public class SwaggerConfig {
    
        //配置swagger的Docket的bean实例
        @Bean
        public Docket docket(){
            return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
        }
    
        //配置swagger信息=apiInfo
        private ApiInfo apiInfo(){
            //作者信息
            Contact contact = new Contact("郝嘉诚", "http://haojiacheng.cn", "990784805@qq.com");
    
            return new ApiInfo("苏弋的swaggerAPI文档",
                    "不积硅步无以至千里",
                    "v1.0",
                    "http://haojiacheng.cn",
                    contact,
                    "Apache 2.0",
                    "http://www.apache.org/licenses/LICENSE-2.0",
                    new ArrayList()
            );
        }
    }
    
    
    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



    ### Swagger配置扫描接口

    Docket.select()

    ~~~java
    //配置swagger的Docket的bean实例
    @Bean
    public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
    .apiInfo(apiInfo())
    .select()
    //RequestHandlerSelectors:配置要扫描接口的方式
    //basePackage:指定要扫描的包
    //any():扫描全部
    //none():不扫描
    //withMethodAnnotation:扫描方法上的注解,参数是注解的反射对象
    //withClassAnnotation:扫描类上的注解
    .apis(RequestHandlerSelectors.basePackage("com.sec.controller"))
    //paths:过滤什么路径
    .paths(PathSelectors.ant("/sec/**"))
    .build();
    }

配置是否启动swagger

1
.enable(false)//为false时则swagger不能在浏览器中访问

我只希望我的swagger在生产环境中使用,在发布的时候不用?

  • 判断是不是生产环境 flag=false
  • 注入enable(flag)

==步骤==:

  1. application.properties: 执行dev

    1
    spring.profiles.active=dev
  2. application-dev.properties:

    1
    server.port=8081
  3. application-prod.properties:

    1
    server.port=8082
  4. SwaggerConfig.java:

    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
    package com.sec.config;

    ....

    @Configuration
    @EnableSwagger2 //开启Swagger2
    public class SwaggerConfig {

    //配置swagger的Docket的bean实例
    @Bean
    public Docket docket(Environment environment){
    //设置要显示的Swagger环境
    Profiles profiles = Profiles.of("dev","test");

    //获取项目的环境:
    //通过environment.acceptsProfiles判断此时是否处于自己设定的环境
    boolean flag = environment.acceptsProfiles(profiles);

    return new Docket(DocumentationType.SWAGGER_2)
    .apiInfo(apiInfo())
    //enable:是否启动swagger
    .enable(flag)//若为false,则在浏览器中不能访问swagger-ui.html
    .select()
    .apis(RequestHandlerSelectors.basePackage("com.sec.controller"))
    .build();
    }

    //配置swagger信息=apiInfo
    private ApiInfo apiInfo(){
    //作者信息
    Contact contact = new Contact("郝嘉诚", "http://haojiacheng.cn", "990784805@qq.com");

    return new ApiInfo("苏弋的swaggerAPI文档",
    "不积硅步无以至千里",
    "v1.0",
    "http://haojiacheng.cn",
    contact,
    "Apache 2.0",
    "http://www.apache.org/licenses/LICENSE-2.0",
    new ArrayList()
    );
    }
    }

配置API文档的分组

1
.groupName("郝嘉诚")

如何配置多个分组;多个Docket即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("张三");
}

@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("李四");
}

@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("王五");
}

Swagger常用注解使用详解

1. Api

@Api 用在类上,说明该类的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Api(value = "hello控制类",tags = "苏弋")
public class HelloController {
@ApiOperation(value = "hello方法",notes = "hello方法的notes")
@GetMapping (value = "/hello")
public String hello(@ApiParam("名字") String name){
return "hello"+name;
}

@ApiOperation(value = "user方法",notes = "user方法的notes")
//只要接口中,返回值有实体类,它就会被扫描到swagger中
@PostMapping(value = "/user")
public User user(){
return new User();
}
}

1620348961215

2. ApiModel

@ApiModel 常用在实体类上

1
2
3
4
5
@ApiModel("用户实体类")
public class User {
public String username;
public String password;
}

1620349115367

3. ApiModelProperty

@ApiModelProperty() 用于字段,表示对 model 属性的说明。

1
2
3
4
5
6
7
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}

1620349122428

4. ApiParam

@ApiParam 用于 Controller 中方法的参数说明。

1
2
3
4
5
@ApiOperation(value = "hello方法",notes = "hello方法的notes")
@GetMapping (value = "/hello")
public String hello(@ApiParam("名字") String name){
return "hello"+name;
}

1620349197404

5. ApiOperation

@ApiOperation 用在 Controller 里的方法上,说明方法的作用

1
2
3
4
5
6
@ApiOperation(value = "user方法",notes = "user方法的notes")
//只要接口中,返回值有实体类,它就会被扫描到swagger中
@PostMapping(value = "/user")
public User user(){
return new User();
}

1620349267574

6. ApiResponse 和 ApiResponses

@ApiResponse 用于方法上,说明接口响应的一些信息;@ApiResponses 组装了多个 @ApiResponse。

1
2
3
4
@ApiResponses({ @ApiResponse(code = 200, message = "OK", response = UserDto.class) })
@PostMapping("/user")
public UserDto addUser(@ApiParam(value = "新增用户参数", required = true) @RequestBody AddUserParam param) {
}

7. ApiImplicitParam 和 ApiImplicitParams

用于方法上,为单独的请求参数进行说明。

1
2
3
4
5
6
7
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户ID", dataType = "string", paramType = "query", required = true, defaultValue = "1") })
@ApiResponses({ @ApiResponse(code = 200, message = "OK", response = UserDto.class) })
@GetMapping("/user")
public UserDto getUser(@RequestParam("id") String id) {
return new UserDto();
}
  • name:参数名,对应方法中单独的参数名称。
  • value:参数中文说明。
  • required:是否必填。
  • paramType:参数类型,取值为 path、query、body、header、form。
  • dataType:参数数据类型。
  • defaultValue:默认值。

Swagger中的Try it out

实体类:

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
package com.sec.pojo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;

public User() {
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

controller:

1
2
3
4
5
@ApiOperation(value = "测试方法",notes = "这是一个测试方法")
@PostMapping("/ceshi")
public User ceshi(User user){
return user;
}

点击 Try it out,输入用户名和密码

1620349920120

点击 execute1620350051071

可以看到方法执行的结果1620350094357

总结:

  • 我们可以通过swagger给一些难理解的属性或接口,添加注释
  • 接口文档实时更新
  • 可以在线测试controller中的方法

==注意==:在正式发布的时候,要关闭swagger,不能被用户所看到。出于安全考虑,节省运行内存;

Redis

NoSQL的四大分类

KV键值对:

  • 新浪:Redis
  • 美团:Redis + Tair(是由淘宝网自主开发的KV结构数据存储系统,在淘宝网有着大规模的应用)
  • 阿里、百度:Redis + memecache(高速缓存系统,与redis相似 )

文档型数据库(bson 和json一样):

  • MongoDB(一般必须要掌握)
    • MongoDB是一个基于分布式文件存储的数据库,C++编写的,只要用来处理大量的文档!
    • MongoDB是一个介于关系型数据库和非关系型数据库中中间的产品!MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的!
  • ConchDB

列存储数据库(分布式的文件系统)

  • HBase
  • 分布式文件系统

图关系数据库 (社交网络,推荐系统等。专注于构建关系图谱)

  • Neo4J
  • InfoGrid
  • infinite Graph

Redis入门

概述:

Redis是什么?

​ Redis(==Re==mote ==d==ictionary ==S==erver),即远程字典服务!

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

免费和开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!

Redis能干什么?

  1. 内存存储、持久化,内存中是断电即失、所以说持久化很重要(edb、aof)
  2. 效率高,可以用于高速缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器、计数器(浏览量!)
  6. ……

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

安装Redis

Windows安装

直接下载Redis64位的解压在自己电脑的环境目录下就OK了

启动服务端,然后使用客户端连接Redis

1637216387583

Linux安装(推荐使用)

1637216699146

1、将redis的Linux版解压安装到Linux服务器上

解压命令:==tar -zxvf redis.tar.gz==

2、redis默认不是后端启动的,修改配置文件

1637218892305

3、启动redis服务

1637219141193

4、使用redis-cli连接测试

1637219189443

5、查看redis的进程是否开启!

1637219245806

6、关闭redis服务 ==shutdown==

性能测试

1
2
# 测试: 100个并发连接   100000请求
redis-benchmark -h localhost -p 6379 -c100 -n 100000

1637221977033

参数

1637222653050

基础的知识

redis默认有16个数据库

1637222911124

默认使用的是第0个

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]>DBSIZE #查看DB大小
(integer) 0
127.0.0.1:6379[3]>keys * #查看数据库所有的key
1) "name"
127.0.0.1:6379[3]>flushdb #清除当前数据库
OK
127.0.0.1:6379[3]>keys *
(empty list or set)
127.0.0.1:6379[3]>flushall #清除全部数据库的内容

Redis 是单线程的!

明白Redis是很快的,官方表示,Reis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!

Redis是C语言写的,官方提供的数据为 100000+的QPS,完全不比同样是使用Key-Value的Memecache差!

Redis 为什么单线程还这么快?

1、误区1:高性能的服务器一定是多线程的?

2、误区2:多线程(CPU上下文会切换!)一定比单线程效率高!

核心:redis是将所有的数据全部放在内存中的,所以使用单线程去操作效率就是最高的多线程(CPU上下文会切换!:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都在一个CPU上,在内存情况下,这个就是最佳的方案!

五大数据类型

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作==数据库==、==缓存==和==消息中间件==。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting), LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Redis-Key

1
2
3
4
5
exists [key] 		#判断当前的key是否存在
move [key] 1 #移动key到1号数据库
expire [key] 10 #设置key的过期时间(单位是秒)
ttl [key] #查看key的剩余时间
type [key] #查看key的类型

String(字符串)

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
append [key] "hello" 		#追加字符串,如果key不存在就相当于set key "hello"
strlen [key] #获取key值的长度

#############################################################################
set [key] 0 #设置key的value为0
incr [key] #增长1
decr [key] #减少1
INCRBY [key] 5 #增加5
DECRBY [key] 4 #减少4
GETRANGE [key] 0 3 #截取字符串[0,3]
GETRANGE [key] 0 -1 #截取全部的字符串 相当于get key
SETRANGE [key] 2 #假设key的value为hello 则结果为
#############################################################################

setex [key] [value] 30 #设置key的值并设置有效期为30秒
setnx [key] [value] #设置key的值,如果key存在,则set失败,反之set成功(常用于分布式锁)

#设置/获取多个值
mset k1 v1 k2 v2 k3 v3 #同时设置多个值
mget k1 k2 k3 #同时获取多个值
msetnx k1 k4 #(k1存在,k4不存在), msetnx是个原子性的操作,要么全成功,要么全失败

#对象
mset user:1:name zhangsan user:1:age 2 #设置一个对象name值为zhangsan,age值为2

#getset 先get后set
getset db redis #如果值不存在则返回nil(此处的set是成功的)
getset db mongodb #如果db值存在则先获取db的值,然后更新db的值为mongodb

List(列表)

基本的数据类型,列表

1637229101337

在redis里面,我们可以把list玩成,栈、队列、阻塞队列!

所有的list命令都是用l开头的,Redis不区分大小写命令

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
########################################################################
Lpush

Lrange
127.0.0.1:6379> LPUSH list one #将一个值或者多个值,插入到列表头部(左)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE 1ist 0 -1 #获取1ist中值!
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 #通过区间获取具体的值!
1) "three"
2) "two"
127.0.0.1:6379> Rpush list right #将一个值或者多个值,插入到列表位部(右)(integer)4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4)"right"
########################################################################
LPOP
RPOP

127.0.0.1:6379> LRANGE 1ist 0 -1
1) "three"
2) "two"
3) "one "
4)"right"
127.0.0.1:6379> Lpop list #移除1ist的第一个元素
"three"
127.0.0.1:6379> Rpop list #移除1ist的最后一个元素
"right"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
########################################################################
Lindex

127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> Lindex list 1 #通过下标获得list中的某一个值!
"one"
127.0.0.1:6379> Lindex list 0
"two"

########################################################################
Llen

127.0.0.1:6379> Lpush list one
(integer) 1
127.0.0.1:6379> Lpush list two
(integer) 2
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> Llen list # 返回列表的长度
(integer) 3

########################################################################
移除指定的值!
取关 uid

Lrem

127.0.0.1:6379> LRANGE 1ist 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> Lrem list 1 one #移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> Lrem list 1 three
(integer) 1
127.0.0.1:6379> LRANGE 1ist 0 -1
1) "three"
2) "two"
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> Lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE 1ist 0 -1
1) "two"

########################################################################
trim 修剪。 让列表只保留指定区间内的元素

127.0.0.1:6379>Rpush mylist "he11o"
(integer) 1
127.0.0.1:6379> Rpush mylist "he11o1"
(integer) 2
127.0.0.1:6379> Rpush mylist "he17o2"
(integer) 3
127.0.0.1:6379> Rpush mylist "he11o3"
(integer) 4
127.0.0.1:6379>Ltrim mylist 1 2 #通过下标截取指定的长度,这个1ist已经被改变了,截断了只剩下截取的元素!oK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "he11o1"
2) "he11o2"

########################################################################
rpoplpush #移除列表最右边的元素并添加到另一个list(从左边往进添加)

127.0.0.1:6379> Lpush list one two three four five
(integer) 5
127.0.0.1:6379> Lrange list 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
5) "one"
127.0.0.1:6379> RpopLpush list mylist
"one"
127.0.0.1:6379> Lrange list 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
127.0.0.1:6379> Lrange mylist 0 -1
1) "one"
127.0.0.1:6379>


########################################################################
lset 将列表中指定下标的值替换为另外一个值,更新操作

127.0.0.1:6379> EXISTS list #判断这个列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item #如果不存在列表我们去更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379>LRANGE list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item #如果存在,更新当前下标的值
oK
127.0.0.1:6379>LRANGE list 0 0
1) "item"
127.0.0.1:6379> lset list 1 other #如果不存在,则会报错
(error) ERR index out of range


########################################################################
Linsert 把值插入到指定值的之前(左)或之后(右)

127.0.0.1:6379>Rpush mylist "hello"
( integer)1
127.0.0.1:6379> Rpush mylist "world"
(integer)2
127.0.0.1:6379>LINSERT mylist before "world" "other"
( integer)3
127.0.0.1:6379>LRANGE mylist 0 -1
1) "hello"
2)"other"
3)"world"
127.0.0.1:6379>LINSERT mylist after world new
(integer)4
127.0.0.1:6379> LRANGE mylist 0 -1
1)"hello"
2)"other"
3)"world"
4)"new"





小结

  • 它实际上是一个链表,before Node after , left , right 都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在!
  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点。

消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)

Set(集合)

set中的值是不能重复的!

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
########################################################################
127.0.0.1:6379> sadd myset "hel1o" #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "kuangshen"
(integer) 1
127.0.0.1:6379> sadd myset "lovekuangshen"
(integer) 1
127.0.0.1:6379> SMEMBERS myset #查看指定set的所有值
1) "hello"
2) "lovekuangshen"
3) "kuangshen"
127.0.0.1:6379> SISMEMBER myset hello #判断某个值是否存在与set集合中
(integer) 1
127.0.0.1:6379>SISMEMBER myset wor1d
(integer) 0

########################################################################
scard

127.0.0.1:6379> scard myset #获取set集合中的内容元素个数!
(integer) 4
########################################################################
srem

127.0.0.1:6379> srem myset hello #移除set集合中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> smembers myset
1) "hello world haojiacheng"
2) "hello world"

########################################################################
set 无序不重复集合。抽随机!
srandmember #随机抽一个元素

127.0.0.1:6379> srandmember myset
"k"
127.0.0.1:6379> srandmember myset
"h"
127.0.0.1:6379> srandmember myset
"hello world haojiacheng"
127.0.0.1:6379> srandmember myset
"a"
127.0.0.1:6379> srandmember myset
"f"
127.0.0.1:6379> srandmember myset
"hello world"
127.0.0.1:6379> srandmember myset 2 #随机抽选出指定个数的元素
1) "j"
2) "n"
127.0.0.1:6379> srandmember myset 2
1) "m"
2) "i"

########################################################################
spop #随即删除一个元素,也可以指定随机删除个数

127.0.0.1:6379> SMEMBERS myset
1) "c"
2) "hello world"
3) "hello world haojiacheng"
4) "o"
5) "e"
6) "l"
7) "i"
8) "b"
9) "h"
10) "n"
11) "g"
12) "d"
13) "j"
14) "p"
15) "k"
16) "m"
17) "q"
18) "a"
19) "f"
127.0.0.1:6379> SPOP myset
"a"
127.0.0.1:6379> SPOP myset
"hello world"
127.0.0.1:6379> SPOP myset
"j"
127.0.0.1:6379> SPOP myset 2
1) "f"
2) "i"
########################################################################
smove #将一个指定的值,移动到另外一个set集合中

127.0.0.1:6379> sadd myset one two three hello haojiacheng
(integer) 5
127.0.0.1:6379> SMEMBERS myset
1) "one"
2) "hello"
3) "three"
4) "two"
5) "haojiacheng"
127.0.0.1:6379> smove myset newmyset haojiacheng
(integer) 1
127.0.0.1:6379> scard myset
(integer) 4
127.0.0.1:6379> SMEMBERS newmyset
1) "haojiacheng"
########################################################################
数字集合类
- 差集 sdiff
- 交集 sinter
- 并集 sunion

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2 #key1中key2没有的元素
1) "a"
2) "b"
127.0.0.1:6379> sdiff key2 key1 #key2中key1没有的元素
1) "d"
2) "e"
127.0.0.1:6379> sinter key1 key2 #key1和key2共同的元素
1) "c"
127.0.0.1:6379> sunion key1 key2 #key1和key2全部元素
1) "e"
2) "b"
3) "c"
4) "a"
5) "d"


微博,A用户将所有关注的人放在一个set集合中,将它的粉丝也放在一个set集合中;这时可以做一些功能:

共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)

Hash(哈希)

Map集合,key-value(map)!这里的value是一个map集合!

set myhash field kuangshen

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
########################################################################
hset hmset hmget hgetall hdel

127.0.0.1:6379> hset myhash field1 haojiacheng #set一个具体 key-value
(integer) 1
127.0.0.1:6379> hget myhash field1
"haojiacheng"
127.0.0.1:6379> hmset myhash field1 hello field2 world #set多个 key-value
OK
127.0.0.1:6379> hmget myhash field1 field2 #获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> Hgetall myhash #获取全部的数据(包含value中的key)
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> HDEL myhash field1 #删除hash指定key字段!对应的value值也就消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
########################################################################
hlen

127.0.0.1:6379> hlen myhash #获取hash表的字段数量
(integer) 1
########################################################################
hexists

127.0.0.1:6379> HEXISTS myhash field1 #判断hash中指定字段是否存在
(integer) 0
127.0.0.1:6379> HEXISTS myhash field2
(integer) 1
########################################################################
hkeys #只获得所有field
hvals #只获得所有value

127.0.0.1:6379> hmset myhash one 1 two 2 three 3 four 4
OK
127.0.0.1:6379> HGETALL myhash
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
127.0.0.1:6379> hkeys myhash
1) "one"
2) "two"
3) "three"
4) "four"
127.0.0.1:6379> hvals myhash
1) "1"
2) "2"
3) "3"
4) "4"

########################################################################
HINCRBY hsetnx

127.0.0.1:6379> HGETALL myhash
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
127.0.0.1:6379> hset myhash five 5
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "four"
8) "4"
9) "five"
10) "5"
127.0.0.1:6379> HINCRBY myhash five 1 #增长1
(integer) 6
127.0.0.1:6379> HINCRBY myhash five -1 #减少1
(integer) 5
127.0.0.1:6379> hsetnx myhash sex 6 #存在则创建失败,不存在则创建
(integer) 1
127.0.0.1:6379> hsetnx myhash sex nihao
(integer) 0

hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!

hash更适合存储对象,String更适合存储字符串

Zset(有序集合)

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
########################################################################
zadd zrangebyscore(排序--小到大) ZREVRANGE(排序--大到小)
127.0.0.1:6379> clear
127.0.0.1:6379> zadd salary 2500 xiaohong #添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 200 suyi
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf #显示全部的用户 从小到大
1) "suyi"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #显示全部的用户 从大到小
1) "zhangsan"
2) "libai"
3) "suyi"
127.0.0.1:6379> ZREVRANGE salary 0 -1 withscores #从大到小排序全部的用户附带薪水
1) "zhangsan"
2) "5000"
3) "libai"
4) "3000"
5) "suyi"
6) "200"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #显示全部的用户并附带薪水
1) "suyi"
2) "200"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores #显示工资小于等于2500的用户即薪水
1) "suyi"
2) "200"
3) "xiaohong"
4) "2500"
########################################################################
zrem #移除
zcard #获取有序集合中的个数

127.0.0.1:6379> zrange salary 0 -1
1) "suyi"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong #移除小红
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "suyi"
2) "zhangsan"
127.0.0.1:6379> zcard salary #获取有序集合中的个数
(integer) 2
########################################################################
zount #获取指定区间的成员数量!
127.0.0.1:6379> zadd myzset 1 hello 2 world 3 !
(integer) 3
127.0.0.1:6379> zcount myzset 0 -1
(integer) 0
127.0.0.1:6379> zcount myzset 0 4
(integer) 3
127.0.0.1:6379> zcount myzset 0 6
(integer) 3
127.0.0.1:6379> zcount myzset 0 2
(integer) 2
127.0.0.1:6379> zcount myzset -inf +inf
(integer) 3



案例思路:

存储班级成绩表,工资表

带权重的消息:普通消息:1;重要消息:2

排行榜 取top N

三种特殊数据类型

geospatial

经纬度在线查询

geoadd

1
2
3
4
5
6
7
8
9
10
11
12
13
#geoadd		添加地理位置
127.0.0.1:6379> geoadd china:city 121.48941 31.40527 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 116.23128 40.22077 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 113.88308 22.55329 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 108.93425 34.23053 xian
(integer) 1
127.0.0.1:6379> geoadd china:city 109.26291 35.57937 huangling
(integer) 1


geopos

1
2
3
4
5
6
7
8
9
#geopos		获取城市经纬度  可获取多个
127.0.0.1:6379> geopos china:city huangling
1) 1) "109.26290899515151978"
2) "35.579370167327248"
127.0.0.1:6379> geopos china:city huangling xian
1) 1) "109.26290899515151978"
2) "35.579370167327248"
2) 1) "108.93425256013870239"
2) "34.23053097599082406"

geodist

  • m表示为米
  • km表示为千米
  • mi表示为英里
  • ft表示为英尺
1
2
3
4
5
6
#geodist 	返回两个地理空间之间的距离
127.0.0.1:6379> GEODIST china:city beijing shanghai
"1088644.3544"
127.0.0.1:6379> GEODIST china:city beijing shanghai km #以千米形式返回
"1088.6444"

georadius 以给定的经纬度为中心,找出某一半径被的元素

我附近的人?(获得所有附近的人的地址,定位)通过半径来查询

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
#返回110经度 30纬度为中心的1000km距离的数据
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "xian"
2) "huangling"
3) "shenzhen"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "xian"

#返回以经纬度110,30为中心500km的数据并附带距离
127.0.0.1:6379> georadius china:city 110 30 500 km withdist
1) 1) "xian"
2) "481.1278"

#返回以经纬度110,30为中心1000km的数据并附带经纬度
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord
1) 1) "xian"
2) 1) "108.93425256013870239"
2) "34.23053097599082406"
2) 1) "huangling"
2) 1) "109.26290899515151978"
2) "35.579370167327248"
3) 1) "shenzhen"
2) 1) "113.88307839632034302"
2) "22.55329111565713873"

#返回以经纬度110,30为中心1000km的数据并附带经纬度和距离且限制返回数据的个数
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord withdist count 2
1) 1) "xian"
2) "481.1278"
3) 1) "108.93425256013870239"
2) "34.23053097599082406"
2) 1) "huangling"
2) "624.3814"
3) 1) "109.26290899515151978"
2) "35.579370167327248"

georadiusbymember 相比于georedius 的区别是 中心元素变成了城市

1
2
3
4
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km 
1) "beijing"
2) "xian"
3) "huangling"

geohash 返回一个或多个位置元素的 Geohash 表示。

该命令返回11个字符的geohash字符串

1
2
3
4
5
6
#将二维的经纬度转换为一维的字符串
#如果两个字符串越接近,则距离越近
127.0.0.1:6379> geohash china:city beijing shanghai
1) "wx4sucvncn0"
2) "wtw6st1uuq0"

GEO底层的实现原理

Zset!

可以使用Zset命令操作geo!

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> zrange china:city 0 -1		#查看地图中全部元素
1) "xian"
2) "huangling"
3) "shenzhen"
4) "shanghai"
5) "beijing"
127.0.0.1:6379> zrem china:city shenzhen #移除指定元素
(integer) 1

hyperloglog

Redis 2.8.9版本就更新了Hyperloglog 数据结构!
Redis Hyperloglog基数统计的算法!
优点:占用的内存是固定, 2^64不同的元素的技术,只需要废12KB内存! 如果要从内存角度来比较的话Hyperloglog首选!
网页的UV (一个人访问一个网站多次,但是还是算作一个人! )
传统的方式,set保存用户的id ,然后就可以统计set中的元素数量作为标准判断!
这个方式如果保存大量的用户id ,就会比较麻烦!我们的目的是为了计数,而不是保存用户id ;
0.81%错误率!统计UV任务,可以忽略不计的!

测试使用

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> PFadd mykey a b c d e f g h i j		#创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey #统计 mykey 元素的基数数量
(integer) 10
127.0.0.1:6379> PFadd mykey2 i j z x c v b n m #创建第二组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 #合并两组 mykey mykey2=》 mykey3 并集
OK
127.0.0.1:6379> PFCOUNT mykey3 #查看并集的数量
(integer) 15

如果允许容错,那么一定可以使用Hyperloglog

如果不允许容错,就使用set或者自己的数据类型即可!

bitmap

位存储

统计用户信息,活跃,不活跃!登录、未登录!打卡,365打卡!两个状态的,都可以使用Bitmaps!

Bitmaps 位图,数据结构! 都是操作二进制位来进行记录,就只有0和1两个状态!

365天=365 bit 1字节=8 bit 46个字节左右!

测试

使用Bitmaps 来记录 周一到周日的打卡!

周一:1 周二:0 周三:0 周四:1……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

查看某一天是否有打卡!

1
2
3
4
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 6
(integer) 0

统计操作,统计打卡的天数!

1
2
127.0.0.1:6379> bitcount sign  #统计这周的打卡记录,就可以看到是否有全勤
(integer) 3

事务

要么都成功,要么都失败,原子性!

Redis事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!

一次性、顺序性、排他性!执行一系列的命令

1
---------- 队列 set  set  set  执行----------

==Redis事务没有隔离级别的概念==

所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行!Exec

==Redis单条命令是保存原子性的,但是Redis的事务是不保证原子性的!==

redis事务

  • 开启事务(multi)
  • 命令入队(…)
  • 执行事务(exec)

正常执行事务!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name zhangsan
QUEUED
127.0.0.1:6379(TX)> set age 20
QUEUED
127.0.0.1:6379(TX)> get name
QUEUED
127.0.0.1:6379(TX)> set sex 0
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "zhangsan"
4) OK

放弃事务

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> discard #取消事务
OK
127.0.0.1:6379> get k1
(nil)

编译型异常(代码有问题,命令有错误) 事务中所有的命令都不会被执行!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> exec #执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5 #所有的命令都不会执行!
(nil)

运行时异常(1/0),如果事务队列中存在语法性错误,那么执行命令的时候,其他命令正常执行,错误命令抛出异常!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> set k1 "v1"			#设置一个字符串
OK
127.0.0.1:6379> MULTI #进入事务
OK
127.0.0.1:6379(TX)> INCR k1 #自增1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> EXEC #执行
1) (error) ERR value is not an integer or out of range #运行时异常,字符串不能自增
2) OK #其余命令执行成功
3) OK
4) "v3"

监控!Watch (面试常问!)

悲观锁

  • 很悲观,认为什么时候都会出问题,无论做什么都会加锁!

乐观锁

  • 很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候取判断一下,在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version

Redis的监视测试

正常执行成功!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> WATCH money #监视 money 对象
OK
127.0.0.1:6379> multi #事务正常结束,数据期间没有发生变动,这时就正常执行成功!
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用watch可以当作redis的乐观锁操作!

==在客户端2监视money,进入事务后,客户端1修改了money,此时客户端2的事务必定执行失败!==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#客户端1:
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000
OK

#客户端2:
127.0.0.1:6379> watch money #监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby money 10
QUEUED
127.0.0.1:6379(TX)> exec #执行前,另外一个线程修改了我们的值,这个时候,就会导致事务执行失败!
(nil)
127.0.0.1:6379> get money
"1000"

==如果修改失败,重新监视一下就ok==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 1
QUEUED
127.0.0.1:6379(TX)> incrby out 1
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 999
2) (integer) 21

Jedis

Jedis是Redis官方推荐的 java 连接开发工具! 使用 Java 操作Redis中间件!

测试

1.导入依赖

1
2
3
4
5
6
7
8
9
10
11
<!--导入jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>

2.编码测试

  • 连接数据库
  • 操作命令
  • 断开连接
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sec;

import redis.clients.jedis.Jedis;

public class TestPing {
public static void main(String[] args) {
//1. new Jedis 对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//jedis 所有的命令与redis的命令一样
System.out.println(jedis.ping());
}
}

输出:

1637388595177

常用的API

String

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
package com.sec.base;

import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class TestString {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);

jedis.flushDB();
System.out.println("===========增加数据===========");
System.out.println(jedis.set("key1","value1"));//OK
System.out.println(jedis.set("key2","value2"));//OK
System.out.println(jedis.set("key3", "value3"));//OK
System.out.println("删除键key2:"+jedis.del("key2"));//1
System.out.println("获取键key2:"+jedis.get("key2"));//null
System.out.println("修改key1:"+jedis.set("key1", "value1Changed"));//OK
System.out.println("获取key1的值:"+jedis.get("key1"));//value1Changed
System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));//9
System.out.println("key3的值:"+jedis.get("key3"));//value3End
System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03"));//OK
System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));//[value01, value02, value03]
System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04"));//[value01, value02, value03, null]
System.out.println("删除多个键值对:"+jedis.del("key01","key02"));//2
System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));//[null, null, value03]

jedis.flushDB();
System.out.println("===========新增键值对防止覆盖原先值==============");
System.out.println(jedis.setnx("key1", "value1"));//1
System.out.println(jedis.setnx("key2", "value2"));//1
System.out.println(jedis.setnx("key2", "value2-new"));//0
System.out.println(jedis.get("key1"));//value1
System.out.println(jedis.get("key2"));//value2

System.out.println("===========新增键值对并设置有效时间=============");
System.out.println(jedis.setex("key3", 2, "value3"));
System.out.println(jedis.get("key3"));//value3
try {
TimeUnit.SECONDS.sleep(3);//休眠3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(jedis.get("key3"));//null

System.out.println("===========获取原值,更新为新值==========");
System.out.println(jedis.getSet("key2", "key2GetSet"));//value2
System.out.println(jedis.get("key2"));//key2GetSet

System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4));//y2G
}
}

List

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
package com.sec.base;

import redis.clients.jedis.Jedis;

public class TestList {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("===========添加一个list===========");
jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
jedis.lpush("collections", "HashSet");
jedis.lpush("collections", "TreeSet");
jedis.lpush("collections", "TreeMap");
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
System.out.println("collections区间0-3的元素:"+jedis.lrange("collections",0,3));
System.out.println("===============================");
// 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
System.out.println("删除指定元素个数:"+jedis.lrem("collections", 2, "HashMap"));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("删除下表0-3区间之外的元素:"+jedis.ltrim("collections", 0, 3));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(左端):"+jedis.lpop("collections"));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("collections添加元素,从列表右端,与lpush相对应:"+jedis.rpush("collections", "EnumMap"));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("collections列表出栈(右端):"+jedis.rpop("collections"));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections", 1, "LinkedArrayList"));
System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
System.out.println("===============================");
System.out.println("collections的长度:"+jedis.llen("collections"));
System.out.println("获取collections下标为2的元素:"+jedis.lindex("collections", 2));
System.out.println("===============================");
jedis.lpush("sortedList", "3","6","2","0","7","4");
System.out.println("sortedList排序前:"+jedis.lrange("sortedList", 0, -1));
System.out.println(jedis.sort("sortedList"));
System.out.println("sortedList排序后:"+jedis.lrange("sortedList", 0, -1));
}
}

Set

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
package com.sec.base;

import redis.clients.jedis.Jedis;

public class TestSet {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println("============向集合中添加元素(不重复)============");
System.out.println(jedis.sadd("eleSet", "e1","e2","e4","e3","e0","e8","e7","e5"));//8
System.out.println(jedis.sadd("eleSet", "e6"));//1
System.out.println(jedis.sadd("eleSet", "e6"));//1
System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));//[e1, e4, e2, e0, e5, e8, e3, e7, e6]
System.out.println("删除一个元素e0:"+jedis.srem("eleSet", "e0"));//1
System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));//[e4, e2, e5, e8, e3, e1, e7, e6]
System.out.println("删除两个元素e7和e6:"+jedis.srem("eleSet", "e7","e6"));//2
System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));//[e5, e8, e3, e1, e2, e4]
System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));//e4
System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));//e1
System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));//[e2, e3, e5, e8]
System.out.println("eleSet中包含元素的个数:"+jedis.scard("eleSet"));//4
System.out.println("e3是否在eleSet中:"+jedis.sismember("eleSet", "e3"));//true
System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e1"));//false
System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e5"));//true
System.out.println("=================================");
System.out.println(jedis.sadd("eleSet1", "e1","e2","e4","e3","e0","e8","e7","e5"));//8
System.out.println(jedis.sadd("eleSet2", "e1","e2","e4","e3","e0","e8"));//6
System.out.println("将eleSet1中删除e1并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e1"));//移到集合元素 1
System.out.println("将eleSet1中删除e2并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e2"));//1
System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));//[e7, e4, e0, e5, e8, e3]
System.out.println("eleSet3中的元素:"+jedis.smembers("eleSet3"));//[e2, e1]
System.out.println("============集合运算=================");
System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));//[e7, e4, e0, e5, e8, e3]
System.out.println("eleSet2中的元素:"+jedis.smembers("eleSet2"));//[e1, e3, e4, e2, e0, e8]
System.out.println("eleSet1和eleSet2的交集:"+jedis.sinter("eleSet1","eleSet2"));//[e3, e4, e0, e8]
System.out.println("eleSet1和eleSet2的并集:"+jedis.sunion("eleSet1","eleSet2"));//[e1, e4, e2, e0, e5, e8, e3, e7]
System.out.println("eleSet1和eleSet2的差集:"+jedis.sdiff("eleSet1","eleSet2"));//eleSet1中有,eleSet2中没有 [e7, e5]
jedis.sinterstore("eleSet4","eleSet1","eleSet2");//求交集并将交集保存到eleSet4的集合
System.out.println("eleSet4中的元素:"+jedis.smembers("eleSet4"));//[e0, e8, e3, e4]
}
}

Hash

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
package com.sec.base;

import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

public class TestHash {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
Map<String,String> map = new HashMap<String,String>();
map.put("key1","value1");
map.put("key2","value2");
map.put("key3","value3");
map.put("key4","value4");
//添加名称为hash(key)的hash元素
jedis.hmset("hash",map);
//向名称为hash的hash中添加key为key5,value为value5元素
jedis.hset("hash", "key5", "value5");
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));//return Map<String,String>
System.out.println("散列hash的所有键为:"+jedis.hkeys("hash"));//return Set<String>
System.out.println("散列hash的所有值为:"+jedis.hvals("hash"));//return List<String>
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 6));
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 3));
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("删除一个或者多个键值对:"+jedis.hdel("hash", "key2"));
System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
System.out.println("散列hash中键值对的个数:"+jedis.hlen("hash"));
System.out.println("判断hash中是否存在key2:"+jedis.hexists("hash","key2"));
System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3"));
System.out.println("获取hash中的值:"+jedis.hmget("hash","key3"));
System.out.println("获取hash中的值:"+jedis.hmget("hash","key3","key4"));
}
}

Zset

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
package com.sec.base;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.ZAddParams;

public class TestZset {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
System.out.println(jedis.zadd("score", 80, "shuXue"));
System.out.println(jedis.zadd("score", 90, "yuWen"));
System.out.println(jedis.zadd("score", 120, "yingYu"));
System.out.println(jedis.zadd("score", 100, "wuLi"));
System.out.println(jedis.zadd("score", 150, "shengWu"));
System.out.println("从小到大排序:"+jedis.zrangeByScoreWithScores("score","-inf","+inf"));//从小到大排序 -inf负无穷 +inf正无穷
jedis.zaddIncr("score",5,"shuXue", ZAddParams.zAddParams());//数学成绩+5
System.out.println("数学成绩+5后:"+jedis.zrangeByScoreWithScores("score","-inf","+inf"));
System.out.println("score的个数:"+jedis.zcard("score"));//5
System.out.println("从大到小排序:"+jedis.zrevrangeByScoreWithScores("score","+inf","-inf"));//从大到小排序
System.out.println("删除一个指定元素shengWu:"+jedis.zrem("score","shengWu"));
System.out.println("结果:"+jedis.zrange("score",0,-1));
//………………
jedis.flushDB();
}
}

SpringBoot整合

SpringBoot操作数据︰spring-data jpa jdbc mongodb redis !

SpringData也是和SpringBoot齐名的项目!

说明︰在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce

jedis :采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池!更像BIO模式

lettuce :采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式

源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")//我们可以自己定义一个redisTemplate来替换这个默认的!
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的 RedisTemplate没有过多的设置,redis对象都是需要序列化!
//两个泛型都是 object,object的类型,我们后使用需要强制转换<String,object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean //由于string是redis中最常使用的类型,所以说单独提出来了一个bean !
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

整合测试一下

1.导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.配置连接

1
2
3
#配置redis
spring.redis.port=6379
spring.redis.host=127.0.0.1

3.测试

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
@SpringBootTest
class Redis02SpringbootApplicationTests {

@Autowired
RedisTemplate<Object,Object> redisTemplate;

@Test
void contextLoads() {
/**
* redisTempLate操作不同的数据类型,api和我们的指令是一样的opsForVaLue操作字符串类似String
* opsForList操f作List类似List
* opsForSet
* opsForHash
* opsForZSet
* opsForGeo
* opsForHyperLogLog
*
* 除了基本的操作,我们常用的方法都可以直接通过 redisTemplate 操作,比如事务,和基本的CRUD
*
* 获取redis的连接对象
* RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
* connection.flushDb();
* connection.flushAll();
*/

redisTemplate.opsForValue().set("springboot-redis","haoJiaCheng");
System.out.println(redisTemplate.opsForValue().get("springboot-redis"));
}

}

1637403065944

1637403261311

关于对象的保存 需要序列化,要么

1
String jsonUser = new ObjectMapper().writeValueAsString(user);

要么

②:继承Serializable接口

1637405907640

要么

③:编写自己的redisTemplate

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
@Configuration
public class RedisConfig {

// 自己定义一个 RedisTemplate
// 固定的模板
@Bean
public RedisTemplate<String, Object> myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 开发中一般使用 <String, Object> 的形式
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//enableDefaultTyping过期 使用activateDefaultTyping代替 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key使用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
//以hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

Redis.conf详解

基本配置

位置

Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf

1
config get * # 获取全部的配置

我们一般情况下,会单独拷贝出来一份进行操作。来保证初始文件的安全。

启动的时候,就通过配置文件来启动的!

单位 容量单位不区分大小写,G和GB有区别

==配置文件 unit单位对大小写不敏感==

1637408016127

include  包含,组合多个配置

1637408093417

就好比我们学习Spring、Import、include

NETWORK  网络

1
2
3
bind 127.0.0.1		#绑定的IP
protected-mode yes #保护模式
port 6379 #端口

GENERAL 通用

1
2
3
4
5
6
7
8
9
10
11
12
13
daemonize yes   	#以守护进程的方式运行,默认是no,我们需要修改为yes

pidfile /var/run/redis.pid #如果以后台的方式运行,我们就需要指定一个 pid 文件

#Specify: the server verbosity level.# This can be. one of :
# debug (a lot of information, useful for development/testing)
# verbose(many rarely useful info,but not a mess like the debug level)
# notice (moderately verbose,what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" #日志的文件位置名
databases 16 #数据库的数量,默认是16个
always-show-logo yes #是否显示logo

SNAPSHOPTING  快照,持久化规则

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.rdb. aof

redis是内存数据库,如果没有持久化,那么数据断电及失!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如果900s内,如果至少有一个1 key进行了修改,我们就进行持久化操作
save 900 1
# 如果300s内,如果至少10 key进行了修改,我们就进行持久化操作
save 300 10
#如果60s内,如果至少10000 key进行了修改,我们就进行持久化操作
save 60 10000
#我们之后学习持久化,会自己定义这个测试!

stop-writes-on-bgsave-error yes #持久化如果出错,是否还需要继续工作!

rdbcompression yes #是否压缩 rdb 文件,需要消耗一些cpu资源!

rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验!

dir ./ #rdb 文件保存的目录

REPLICATION 复制,我们后面讲解主从复制的,时候再进行讲解

SECURITY 安全

可以在这里设置redis的密码,默认是没有密码的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置redis的密码
oK
127.0.0.1:6379> config get requirepass #发现所有的命令都没有权限了
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456#使用密码进行登录!
oK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"

限制 CLIENTS

1
2
3
4
5
6
7
8
9
maxclients	10000		#设置能连接上redis的最大客户端数量
maxmemory <bytes> #配置redis最大的内存容量
maxmemory-policy noeviction #内存到达上限之后的处理策略
1、volatile-7ru: 只对设置了过期时间的key进行LRU(默认值)
2、a7lkeys-7ru : 删除lru算法的key
3、volatile-random: 随机删除即将过期key
4、a7lkeys-random: 随机删除
5、volatile-tt7 : 删除即将过期的
6、noeviction : 永不过期,返回错误

APPEND ONLY 模式 aof配置

1
2
3
4
5
6
appendonly  no  #默认是不开启aof模式的,默认是使用rdb方式持久化的,大部分情况下rdb够用
appendfilename "appendonly.aof" #持久化的文件的名字

#appendfsync always #每次修改都会 sync。消耗性能
appendfsync everysec #每秒执行一次 sync,可能会丢失这1秒的数据!
#appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度是最快的

具体的配置,我们在redis持久化中详细讲解!

Redis持久化

面试和工作,持久化都是重点!

Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!

RDB(Redis DataBase)

什么是RDB

在主从复制中,RDB就是备用了!在从机上面!

1637412209507

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。

Redis会单独创建 ( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置!

==rdb保存的文件是dump.rdb== 都是在我们的配置文件中快照中进行配置的!

1637411139128

1637411238185

触发机制

1、save的规则满足的情况下,会自动触发rdb规则

2、执行flushall命令,也会触发我们的rdb规则!

3、退出redis,也会产生rdb文件(dump.rdb)

备份就自动生成一个 dump.rdb

如何恢复rdb文件!

1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb ,恢复其中的数据

2、查看需要存在的位置

1
2
3
127.0.0.1:6379>  config get dir
1) "dir"
2) "/usr/local/bin" #如果在这个目录下存在 dump .rdb文件,启动就会自动恢复其中的数据

几乎就他自己默认的配置就够用了,但是我们还是需要取学习!

优点:

1、适合大规模的数据恢复!

2、对数据的完整性要求不高!

缺点:

1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有了!

2、fork进程的时候,会占用一定的内存空间!

AOF(Append Only File)

将我们的所有命令都记录下来,恢复的时候把这个文件执行一遍

AOF是什么

1637463927193

以日志的形式来记录每个写操作,将Redis执特过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

==AOF保存的文件是 appendonly.aof 文件==

append

1637464071260

默认是不开启的,需要手动修改为yes

重启redis即可生效!

如果aof文件有错误,redis的服务是启动不起来的,我们需要修复aof文件

redis给我们提供了一个工具 redis-check-aof,使用命令redis-check-aof --fix即可修复aof文件!

1637464666620

如果文件正常,重启就可以直接恢复了!(修复文件有一定的==容错率==,可能会导致一些数据丢失)

1637465408979

重写规则

aof 默认就是文件的无限追加,文件会越来越大!

1637465851437

如果aof文件大小超过mb,太大了,fork一个新的进程来讲我们的文件进行重写!

优点和缺点!

1
2
3
4
5
6
7
appendonly  no  #默认是不开启aof模式的,默认是使用rdb方式持久化的,大部分情况下rdb够用
appendfilename "appendonly.aof" #持久化的文件的名字

#appendfsync always #每次修改都会 sync。消耗性能
appendfsync everysec #每秒执行一次 sync,可能会丢失这1秒的数据!
#appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度是最快的
# rewrite 重写

优点:

  1. 每一次写操作都会同步,数据更加完整!
  2. 每秒同步一次,可能会丢失这1秒的数据!
  3. 从不同不的话,效率是最高的!

缺点:

  1. 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢!
  2. Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化!

扩展

1、RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储

2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

3、==只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化==

4、同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会==优先载入AOF==文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

5、性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 9001这条规则。
  • 如果Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的lO,二是AOF rewrite 的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
  • 如果不Enable AOF,仅靠Master-Slave Repllcation实现高可用性也可以,能省掉一大笔(O,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave 中的RDB文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式∶发送者(pub)发送消息,订阅者(sub)接收消息。

Redis 客户端可以订阅任意数量的频道。

订阅/发布消息图:

1637466638473

下图展示了频道channel1,以及订阅这个频道的三个客户端―—client2、 client5和client1之间的关系∶

1637466705639

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

1637467169517

命令

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。

1637467257656

测试

订阅端:

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> SUBSCRIBE feichidesuyi		#订阅一个频道	feichidesuyi
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "feichidesuyi"
3) (integer) 1
#等待读取推送的信息
1) "message" #消息
2) "feichidesuyi" #消息的频道
3) "Hello,SuYi" #消息的内容

发布端:

1
2
127.0.0.1:6379> PUBLISH feichidesuyi "Hello,SuYi"	#发布者发布消息到feichidesuyi 频道
(integer) 1

Pub/Sub从字面上理解就是发布( Publish )与订阅( Subscribe ),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

使用场景:

  1. 实时消息系统
  2. 实时聊天(频道当作聊天室,将信息回显给所有人即可)
  3. 订阅,关注系统

稍微复杂一点的场景,我们会使用,消息中间件MQ()

Redis主从复制

概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用主要包括︰

1、数据冗余∶主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3、负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载﹔尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4、高可用(集群)基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机,一主二从),原因如下︰

1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;

2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,==单台Redis最大使用内存不应该超过20G==。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是”多读少写”。

对于这种场景,我们可以使如下这种架构∶

1637469069556

主从复制,读写分离,80%的情况下都是在进行读操作!减缓服务器的压力,架构中经常使用!一主二从!

只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis !

环境配置

只配置从库,不用配置主库!

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> info replication		#查看当前库的信息
# Replication
role:master #角色 master
connected_slaves:0 #没有从机
master_failover_state:no-failover
master_replid:20506729ff7cf5007efc95bd0548684d1f9758f2
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制3个配置文件,然后修改对应的信息

1
2
3
[root@VM-8-8-centos redis]# cp redis.conf redis79.conf
[root@VM-8-8-centos redis]# cp redis.conf redis80.conf
[root@VM-8-8-centos redis]# cp redis.conf redis81.conf

1637474714104

1、端口 79(主) 80 81

1637474950820

2、pid 名字

1637474964319

3、log文件名字

1637475212805

4、dump.rdb 名字

1637475222116

服务启动:

1
2
3
redis-server redis79.conf
redis-server redis80.conf
redis-server redis81.conf

1637475373992

一主二从(第一种)

1637477718851

==默认情况下,每台Redis服务器都是主节点;==

我们一般情况下只用配置从机就好了!

认老大!一主(79)二从(80,81)

使用SLAVEOF host port就可以为从机配置主机了。

客户端连接:

79:1637475597283

80:1637475635748

81:1637475664508

从机配置(80,81)

1637475685618

成功后,查看信息

1637475842925

真实的从主配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时的!

细节

主机负责写(也可以读)

1637476419150

从机负责读(只能读)

1637476450168

测试 (高可用)

  • 主机断开后,从机依然可以获取值,但是这时没有写操作了。如果主机又回来了,从机依旧可以获取主机写进去的数据

  • 如果是用命令行配置的主从复制,这时,从机断开又重连后,从机将成为主机,此时也拿不到在断开期间,主机写进去的数据,除非再次将它变为从机,即可重新获取到主机的所有值

复制原理

slave启动成功连接到master后会发送一个sync命令

Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,==master将传送整个数据文件到slave,并完成一次完全同步。==

==全量复制==:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

==增量复制==:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

层层链路(第二种)

1637477767479

这时也可以完成我们的主从复制

测试

==谋朝篡位==

如果在层层链路的情况下,主机断开了,这时可以使用 slaveof no one来使自己变成主机,如果自己之前还有从机连接着,则连接关系不变。如果这个时候真正的主机恢复了,真正的主机也没有从机了,需要重新手动连接;

哨兵模式

(自动选举老大的模式)

概述

主从切换技术的方法是∶当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。

谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

1637559953072

这里的哨兵有两个作用

  1. 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  2. 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监
控,这样就形成了多哨兵模式。

1637560056785

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

测试

我们目前的状态是一主二从

1、配置哨兵配置文件 sentinel.conf

1
2
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

后面的1代表,主机挂了,salve投票看让谁接替成为主机,票数最多的,就会成为主机!

2、启动哨兵

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
[root@VM-8-8-centos src]# redis-sentinel sentinel.conf
2808:X 22 Nov 2021 14:10:07.614 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2808:X 22 Nov 2021 14:10:07.614 # Redis version=6.2.4, bits=64, commit=00000000, modified=0, pid=2808, just started
2808:X 22 Nov 2021 14:10:07.614 # Configuration loaded
2808:X 22 Nov 2021 14:10:07.615 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2808
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'

2808:X 22 Nov 2021 14:10:07.621 # Sentinel ID is 2d14dac2e1d9a113b782c01050f441fab229476c
2808:X 22 Nov 2021 14:10:07.621 # +monitor master myredis 127.0.0.1 6379 quorum 1
2808:X 22 Nov 2021 14:10:07.622 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
2808:X 22 Nov 2021 14:10:07.628 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379

如果Master节点断开了,这个时候就会从从机中随机选择一个服务器!(这里面有一个投票算法!)

1637561714121

哨兵日志

1637561686213

此时如果真正的主机回来了,它也只能成为6380的从机

1637561805307

哨兵日志

1637561952057

哨兵模式的全部配置

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
# Example sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 哨兵sentinel的工作目录
dir /tmp

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd


# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1



# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION

#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh

# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

Redis缓存穿透和雪崩

服务的高可用问题!

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。

另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

1637563518763

缓存穿透(查不到)

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(商品的秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力

1637564180150

缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

1637564231452

但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(量太大,缓存过期)

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点 key过期后产生的问题。

加互斥锁

分布式锁∶使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

1637563718042

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

1637563796998

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis ,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

A flyway Util

flyway学习笔记

一、flyway工作流程

  1. 项目启动,应用程序完成数据库连接池的建立后,Flyway自动运行。
  2. 初次使用时,flyway会创建一个 flyway_schema_history 表,用于记录sql执行记录。
  3. Flyway会扫描项目指定路径下(默认是 classpath:db/migration )的所有sql脚本,与 flyway_schema_history 表脚本记录进行比对。如果数据库记录执行过的脚本记录,与项目中的sql脚本不一致,Flyway会报错并停止项目执行。
  4. 如果校验通过,则根据表中的sql记录最大版本号,忽略所有版本号不大于该版本的脚本。再按照版本号从小到大,逐个执行其余脚本。

二、如何使用

1、导入maven依赖

1
2
3
4
5
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.1.0</version>
</dependency>

2、在yml中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
# 数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/flyway-demo?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
flyway:
# 是否启用flyway
enabled: true
# 编码格式,默认UTF-8
encoding: UTF-8
# 迁移sql脚本文件存放路径,默认db/migration
locations: classpath:db/migration
# 迁移sql脚本文件名称的前缀,默认V
sql-migration-prefix: V
# 迁移sql脚本文件名称的分隔符,默认2个下划线__
sql-migration-separator: __
# 迁移sql脚本文件名称的后缀
sql-migration-suffixes: .sql
# 迁移时是否进行校验,默认true
validate-on-migrate: true
# 当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表
baseline-on-migrate: true

3、根据配置文件中的SQL脚本文件存放路径,创建对应的目录

resources/db/migration

4、添加SQL脚本,创建时注意命名规范

  • sql脚本的命名有两种,一种是以V开头的+版本号+两段下划线+文件名.sql,这种以V开头的脚本是只执行一次的,另一种是R开头的,命名方式类似,这种脚本可以重复执行,但不建议修改执行过的sql语句

  • V开头的sql文件比R开头的sql文件优先级高

5、启动项目后可以在flyway_schema_history表中查看相关语句执行记录

6、说明

如果我们修改V2__add_user.sql中的内容,再次执行的话,就会报错,提示信息如下:

  [ERROR] Migration checksum mismatch for migration version 2

  如果我们修改了R__add_unknown_user.sql,再次执行的话,该脚本就会再次得到执行,并且flyway的历史记录表中也会增加本次执行的记录。

三、Maven插件

1、配置

以上步骤中,每次想要migration都需要运行整个springboot项目,并且只能执行migrate一种命令,其实flyway还是有很多其它命令的。maven插件给了我们不需要启动项目就能执行flyway各种命令的机会。

在pom.xml中引入flyway的插件,同时配置好对应的数据库连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<plugins>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>5.2.4</version>
<configuration>
<url>jdbc:mysql://localhost:3306/flyway-demo?characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai
</url>
<user>root</user>
<password>root</password>
<driver>com.mysql.cj.jdbc.Driver</driver>
</configuration>
</plugin>
</plugins>
</build>

2、命令说明

  1. baseline

    对已经存在数据库Schema结构的数据库一种解决方案。实现在非空数据库新建MetaData表,并把Migrations应用到该数据库;也可以在已有表结构的数据库中实现添加Metadata表。

  2. clean

    清除掉对应数据库Schema中所有的对象,包括表结构,视图,存储过程等,clean操作在dev 和 test阶段很好用,但在生产环境务必禁用。

  3. info

    用于打印所有的Migrations的详细和状态信息,也是通过MetaData和Migrations完成的,可以快速定位当前的数据库版本。

  4. repair

    repair操作能够修复metaData表,该操作在metadata出现错误时很有用。

  5. undo

    撤销操作,社区版不支持。

  6. migrate

    迁移。

  7. validate

    验证已经apply的Migrations是否有变更,默认开启的,原理是对比MetaData表与本地Migrations的checkNum值,如果值相同则验证通过,否则失败。

四、知识补充

  1. flyway执行migrate必须在空白的数据库上进行,否则报错。
  2. 对于已经有数据的数据库,必须先baseline,然后才能migrate。
  3. clean操作是删除数据库的所有内容,包括baseline之前的内容。
  4. 尽量不要修改已经执行过的SQL,即便是R开头的可反复执行的SQL,它们会不利于数据迁移。
  5. 当需要做数据迁移的时候,更换一个新的空白数据库,执行下migrate命令,所有的数据库更改都可以一步到位地迁移过去。

五、配置清单

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
flyway.baseline-description对执行迁移时基准版本的描述.
flyway.baseline-on-migrate当迁移时发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移,默认false.
flyway.baseline-version开始执行基准迁移时对现有的schema的版本打标签,默认值为1.
flyway.check-location检查迁移脚本的位置是否存在,默认false.
flyway.clean-on-validation-error当发现校验错误时是否自动调用clean,默认false.
flyway.enabled是否开启flywary,默认true.
flyway.encoding设置迁移时的编码,默认UTF-8.
flyway.ignore-failed-future-migration当读取元数据表时是否忽略错误的迁移,默认false.
flyway.init-sqls当初始化好连接时要执行的SQL.
flyway.locations迁移脚本的位置,默认db/migration.
flyway.out-of-order是否允许无序的迁移,默认false.
flyway.password目标数据库的密码.
flyway.placeholder-prefix设置每个placeholder的前缀,默认${.
flyway.placeholder-replacementplaceholders是否要被替换,默认true.
flyway.placeholder-suffix设置每个placeholder的后缀,默认}.
flyway.placeholders.[placeholder name]设置placeholder的value
flyway.schemas设定需要flywary迁移的schema,大小写敏感,默认为连接默认的schema.
flyway.sql-migration-prefix迁移文件的前缀,默认为V.
flyway.sql-migration-separator迁移脚本的文件名分隔符,默认__
flyway.sql-migration-suffix迁移脚本的后缀,默认为.sql
flyway.tableflyway使用的元数据表名,默认为schema_version
flyway.target迁移时使用的目标版本,默认为latest version
flyway.url迁移时使用的JDBC URL,如果没有指定的话,将使用配置的主数据源
flyway.user迁移数据库的用户名
flyway.validate-on-migrate迁移时是否校验,默认为true

原文链接