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

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


  • 首页

  • 归档

  • 搜索

【SpringBoot DB 系列】Mybatis-Plus

发表于 2021-01-17

【SpringBoot DB 系列】Mybatis-Plus 多数据源配置

前面介绍了两种 Mybatis 的数据源配置,当然也少不了 mybatis-plus

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,既然做增强,那多数据源这种硬性场景,肯定是有非常简单的解决方案的

本文将实例演示 Mybatis-Plus 多数据源的配置

I. 环境准备

1. 数据库相关

以 mysql 为例进行演示说明,因为需要多数据源,一个最简单的 case 就是一个物理库上多个逻辑库,本文是基于本机的 mysql 进行操作

创建数据库test 与 story,两个库下都存在一个表money (同名同结构表,但是数据不同哦)

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

下面是核心的pom.xml(源码可以再文末获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>

配置文件信息application.yml,请注意下面的写法格式,如有疑问可以参考官方教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
datasource:
dynamic:
primary: story #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

II. 项目演示

本文主要参考自 Mybatis-Plus 官方教程,如后续版本有啥变动,请以官方说明为准
mp.baomidou.com/guide/dynam…

1. 实体类

mybatis-plus 可以借助插件实现自动生成相应的代码,我们这里简单自主实现测试 demo,因为两个数据库中表结构完全一致,所以只需要一个 Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Data
@Accessors(chain = true)
@TableName(value = "money")
public class MoneyPo {
/**
* 指定自增策略
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

private String name;

private Long money;

@TableField("is_deleted")
private Integer isDeleted;

@TableField(value = "create_at")
private Timestamp createAt;

@TableField(value = "update_at")
private Timestamp updateAt;
}

2. Mapper 接口

数据库操作定义接口MoneyMapper

1
2
java复制代码public interface MoneyMapper extends BaseMapper<MoneyPo> {
}

对应的 xml 文件resources/mapper/money-mapper.xml

1
2
3
4
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.boot.multi.datasource.mapper.MoneyMapper">
</mapper>

3. Service 接口与实现

因为两张表,所以我们可以定义一个接口,两个不同的实现

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface MoneyService extends IService<MoneyPo> {
}

@Service
@DS("story")
public class StoryMoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {
}

@Service
@DS("test")
public class TestMoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {
}

请注意上面 Service 的注解@DS,value 为前面数据源配置文件中的 key(spring.datasource.dynamic.datasource下面的story + test)

这个注解可以放在类上也可以放在方法上,方法上的优先级 > 类,所以上面的两个 Service 实现可以改成一个

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class MoneyServiceImpl extends ServiceImpl<MoneyMapper, MoneyPo> implements MoneyService {

@DS("story")
public List<MoneyPo> findByStoryIds(Collection<Long> ids) {
return baseMapper.selectBatchIds(ids);
}

@DS("test")
public List<MoneyPo> findByTestIds(Collection<Long> ids) {
return baseMapper.selectBatchIds(ids);
}
}

4. 测试

为简单起见,直接在启动类中添加写上测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SpringBootApplication
@MapperScan("com.git.hui.boot.multi.datasource.mapper")
public class Application {

public Application(TestMoneyServiceImpl testMoneyService, StoryMoneyServiceImpl storyMoneyService) {
List<MoneyPo> moneyPoList = testMoneyService.listByIds(Arrays.asList(1, 1000));
System.out.println(moneyPoList);
System.out.println("--------------");

moneyPoList = storyMoneyService.listByIds(Arrays.asList(1, 1000));
System.out.println(moneyPoList);
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

II. 其他

0. 项目

相关博文

  • 【DB 系列】Mybatis 基于 AbstractRoutingDataSource 与 AOP 实现多数据源切换
  • 【DB 系列】Mybatis 多数据源配置与使用
  • 【DB 系列】JdbcTemplate 之多数据源配置与使用
  • 【DB 系列】Mybatis-Plus 代码自动生成
  • 【DB 系列】MybatisPlus 整合篇
  • 【DB 系列】Mybatis+注解整合篇
  • 【DB 系列】Mybatis+xml 整合篇

源码

  • 工程:github.com/liuyueyi/sp…
  • 源码: github.com/liuyueyi/sp…

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top
  • 微信公众号: 一灰灰blog

本文转载自: 掘金

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

【SpringBoot DB 系列】Mybatis 基于 A

发表于 2021-01-17

【SpringBoot DB 系列】Mybatis 基于 AbstractRoutingDataSource 与 AOP 实现多数据源切换

前面一篇博文介绍了 Mybatis 多数据源的配置,简单来讲就是一个数据源一个配置指定,不同数据源的 Mapper 分开指定;本文将介绍另外一种方式,借助AbstractRoutingDataSource来实现动态切换数据源,并通过自定义注解方式 + AOP 来实现数据源的指定

I. 环境准备

1. 数据库相关

以 mysql 为例进行演示说明,因为需要多数据源,一个最简单的 case 就是一个物理库上多个逻辑库,本文是基于本机的 mysql 进行操作

创建数据库test 与 story,两个库下都存在一个表money (同名同结构表,但是数据不同哦)

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

下面是核心的pom.xml(源码可以再文末获取)

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

配置文件信息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
yaml复制代码# 数据库相关配置,请注意这个配置和之前一篇博文的不一致,后面会给出原因
spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:


# 日志相关
logging:
level:
root: info
org:
springframework:
jdbc:
core: debug

II. 多数据源配置

强烈建议没有看上一篇博文的小伙伴,先看一下上篇博文 【DB 系列】Mybatis 多数据源配置与使用

在开始之前,先有必要回顾一下之前 Mybatis 多数据源配置的主要问题在哪里

  • 多加一个数据源,需要多一份配置
  • Mapper 文件需要分包处理,对开发人员而言这是个潜在的坑

针对上面这个,那我们想实现的目的也很清晰了,解决上面两个问题

1. AbstractRoutingDataSource

实现多数据源的关键,从名字上就可以看出,它就是用来路由具体的数据源的,其核心代码如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 返回选中的数据源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}

if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}

@Nullable
protected abstract Object determineCurrentLookupKey();

其中determineCurrentLookupKey需要我们自己来实现,到底返回哪个数据源

2. 动态数据源实现

我们创建一个DynamicDataSource继承自上面的抽象类

1
2
3
4
5
6
7
java复制代码public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataBaseType = DSTypeContainer.getDataBaseType();
return dataBaseType;
}
}

注意上面的实现方法,怎样决定具体的返回数据源呢?

一个可考虑的方法是,在 Mapper 文件上添加一个注解@DS,里面指定对应的数据源,然后再执行时,通过它来确定具体需要执行的数据源;

因为上面的实现没有传参,因此我们考虑借助线程上下文的方式来传递信息

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
java复制代码public class DSTypeContainer {
private static final ThreadLocal<String> TYPE = new ThreadLocal<String>();

public static String defaultType;

/**
* 往当前线程里设置数据源类型
*
* @param dataBase
*/
public static void setDataBaseType(String dataBase) {
if (StringUtils.isEmpty(dataBase)) {
dataBase = defaultType;
}
TYPE.set(dataBase);
System.err.println("[将当前数据源改为]:" + dataBase);
}

/**
* 获取数据源类型
*
* @return
*/
public static String getDataBaseType() {
String database = TYPE.get();
System.err.println("[获取当前数据源的类型为]:" + database);
return database;
}

/**
* 清空数据类型
*/
public static void clearDataBaseType() {
TYPE.remove();
}
}

3. 注解实现

上面虽然给出了数据源选择的策略,从线程上下文中获取DataBaseType,但是应该怎样向线程上下文中塞这个数据呢?

我们需要支持的方案必然是在 Sql 执行之前,先拦截它,写入这个DataBaseType,因此我们可以考虑在xxxMapper接口上,定义一个注解,然后拦截它的访问执行,在执行之前获取注解中指定的数据源写入上下文,在执行之后清楚上下文

一个最基础的数据源注解@DS

1
2
3
4
5
6
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DS {
String value() default "";
}

注解拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Aspect
@Component
public class DsAspect {

// 拦截类上有DS注解的方法调用
@Around("@within(DS)")
public Object dsAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
DS ds = (DS) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DS.class);
try {
// 写入线程上下文,应该用哪个DB
DSTypeContainer.setDataBaseType(ds == null ? null : ds.value());
return proceedingJoinPoint.proceed();
} finally {
// 清空上下文信息
DSTypeContainer.clearDataBaseType();
}
}
}

4. 注册配置

接下来就是比较关键的数据源配置了,我们现在需要注册DynamicDataSource,然后将他提供给SqlSessionFactory,在这里,我们希望解决即便多加数据源也不需要修改配置,所以我们调整了一下数据源的配置结构

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码spring:
dynamic:
datasource:
story:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
test:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

然后给出一个加载上面配置的配置类DSProperties

1
2
3
4
5
java复制代码@Data
@ConfigurationProperties(prefix = "spring.dynamic")
public class DSProperties {
private Map<String, DataSourceProperties> datasource;
}

然后我们的AutoConfiguration类的实现方式就相对明确了(建议对比上一篇博文中的配置类)

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
java复制代码@Configuration
@EnableConfigurationProperties(DSProperties.class)
@MapperScan(basePackages = {"com.git.hui.boot.multi.datasource.mapper"},
sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {

@SuppressWarnings("unchecked")
@Bean(name = "dynamicDataSource")
public DynamicDataSource DataSource(DSProperties dsProperties) {
Map targetDataSource = new HashMap<>(8);
dsProperties.getDatasource().forEach((k, v) -> {
targetDataSource.put(k, v.initializeDataSourceBuilder().build());
});
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);

// 设置默认的数据库,下面这个赋值方式写法不太推荐,这里只是为了方便而已
DSTypeContainer.defaultType = (String) targetDataSource.keySet().stream().findFirst().get();
dataSource.setDefaultTargetDataSource(targetDataSource.get(DSTypeContainer.defaultType));
return dataSource;
}

@Bean(name = "SqlSessionFactory")
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*/*.xml"));
return bean.getObject();
}
}

5. 数据库实体类

项目结构图

所有前面的东西属于通用配置相关,接下来给出具体的数据库操作相关实体类、Mapper 类

数据库实体类StoryMoneyEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Data
public class StoryMoneyEntity {
private Integer id;

private String name;

private Long money;

private Integer isDeleted;

private Timestamp createAt;

private Timestamp updateAt;
}

mapper 定义接口 StoryMoneyMapper + TestMoneyMapper

1
2
3
4
5
6
7
8
9
10
11
java复制代码@DS(value = "story")
@Mapper
public interface StoryMoneyMapper {
List<StoryMoneyEntity> findByIds(List<Integer> ids);
}

@DS(value = "test")
@Mapper
public interface TestMoneyMapper {
List<TestMoneyEntity> findByIds(List<Integer> ids);
}

对应的 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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.boot.multi.datasource.mapper.StoryMoneyMapper">
<resultMap id="BaseResultMap" type="com.git.hui.boot.multi.datasource.entity.StoryMoneyEntity">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="money" property="money" jdbcType="INTEGER"/>
<result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
<result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
<result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="money_po">
id, `name`, money, is_deleted, create_at, update_at
</sql>

<select id="findByIds" parameterType="list" resultMap="BaseResultMap">
select
<include refid="money_po"/>
from money where id in
<foreach item="id" collection="list" separator="," open="(" close=")" index="">
#{id}
</foreach>
</select>
</mapper>

<!-- 省略第二个xml文件 内容基本一致-->

数据库操作封装类StoryMoneyRepository + TestMoneyRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Repository
public class StoryMoneyRepository {
@Autowired
private StoryMoneyMapper storyMoneyMapper;

public void query() {
List<StoryMoneyEntity> list = storyMoneyMapper.findByIds(Arrays.asList(1, 1000));
System.out.println(list);
}
}

@Repository
public class TestMoneyRepository {
@Autowired
private TestMoneyMapper testMoneyMapper;

public void query() {
List<TestMoneyEntity> list = testMoneyMapper.findByIds(Arrays.asList(1, 1000));
System.out.println(list);
}
}

6. 测试

最后简单的测试下,动态数据源切换是否生效

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootApplication
public class Application {

public Application(StoryMoneyRepository storyMoneyRepository, TestMoneyRepository testMoneyRepository) {
storyMoneyRepository.query();
testMoneyRepository.query();
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

输出日志如下

6.小结

本文主要给出了一种基于AbstractRoutingDataSource + AOP实现动态数据源切换的实现方式,使用了下面三个知识点

  • AbstractRoutingDataSource实现动态数据源切换
  • 自定义@DS注解 + AOP 指定 Mapper 对应的数据源
  • ConfigurationProperties方式支持添加数据源无需修改配置

II. 其他

0. 项目

相关博文

  • 【DB 系列】Mybatis 多数据源配置与使用
  • 【DB 系列】JdbcTemplate 之多数据源配置与使用
  • 【DB 系列】Mybatis-Plus 代码自动生成
  • 【DB 系列】MybatisPlus 整合篇
  • 【DB 系列】Mybatis+注解整合篇
  • 【DB 系列】Mybatis+xml 整合篇

源码

  • 工程:github.com/liuyueyi/sp…
  • 源码: github.com/liuyueyi/sp…

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top
  • 微信公众号: 一灰灰blog

本文转载自: 掘金

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

Spring Boot 扩展 ApplicationCont

发表于 2021-01-17

ApplicationContext 是 Spring 的核心接口或容器。它的功能很多,通过它,我们可以创建、获取、管理 bean;可以发布事件;可以载资源文件;可以获取容器当前运行的环境。Spring 为了更灵活的配置 ApplicationContext,在容器初始化的过程中,Spring 允许用户修改这个对象,具体的方法就是扩展 ApplicationContextInitializer。

Spring Boot 内置的一些 ApplicationContextInitializer,用于实现 Web 配置,日志配置等功能,本文以 Spring Boot 为例,聊聊 ApplicationContextInitializer 的用法。

ApplicationContextInitializer

ApplicationContextInitializer 是个接口,这个接口中定义了一个 initialize 方法,该方法会在 ApplicationContext 初始化的时候执行。我们可以将其理解为 ApplicationContext 的钩子函数。
@FunctionalInterface 是 JDK1.8 加入的注解,标志这个接口拥有单一的方法。

1
2
3
4
5
6
7
8
9
10
java复制代码@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

/**
* Initialize the given application context.
* @param applicationContext the application to configure
*/
void initialize(C applicationContext);

}

我们在使用的时候,只需要继承 ApplicationContextInitializer,实现 initialize 方法就可以了。initialize 方法的入参是当前的 ApplicationContext,通过 ApplicationContext,我们可以获得当前的 Environment,添加或修改一些值;我们可以调用 addApplicationListener 方法添加监听器。总之,很多初始化的工作可以在这里完成。

1
2
3
4
5
6
7
java复制代码@Order(1)
public class UserInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println("UserInitializer");
}
}

ApplicationContextInitializer 可以有多个,支持 @Order 注解,表示执行顺序,越小越早。

SpringFactoriesLoader 是如何加载 ApplicationContextInitializer 的

ApplicationContextInitializer 的子类想要生效,需要注册到 ApplicationContext 中,Spring Boot 项目启动流程的第一步是创建 SpringApplication 对象,在该对象的构造函数中,程序加载了 ApplicationContextInitializer 的实现类。我们详细了解下这个方法。

1
2
3
4
5
6
7
java复制代码public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {

// 将 ApplicationContextInitializer 的实现类的实例加入 this.initializers 中
...
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
...
}

该方法中,SpringFactoriesLoader 根据接口类型获得实现类的名称,通过反射创建实例,根据 sort 注解的值对实例排序。通过反射,我们可以很容易的获得某个类的构造函数以及注解,所以创建实例和排序是比较简单的。有意思的点在于,SpringFactoriesLoader 是如何找到指定接口的实现类的?

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();

// SpringFactoriesLoader 根据接口类型获得实现类的名称
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));

// 反射创建实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);

// 根据 sort 注解的值排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

SpringFactoriesLoader 是 Spring 提供的一种加载方式。说白了也很简单,就是 SpringFactoriesLoader 会固定加载 classpath 路径下的 META-INF/spring.factories 文件,约定该文件中按照 Properties 格式填写好接口和实现类的全名,如果有多个实现类,用逗号隔开。SpringFactoriesLoader 在 Spring Boot 中的作用非常重要,它不仅是加载初始化器的,后续的加载监听器,分析器,前置处理或后置处理器,使用的都是 SpringFactoriesLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
...
try {
// 本质就是调用 classLoader.getResources 方法
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
...
}
}
...
}

如下就是我们将上面写的 UserInitializer 注册在了 ApplicationContext 中,Spring Boot 启动的时候会执行 UserInitializer 的 initialize 方法。

1
ini复制代码org.springframework.context.ApplicationContextInitializer=xxx.xxx.UserInitializer

有没有发现这种通过接口获得类实例的方式和 JDBC 有点像。这其实都属于 java 的 SPI 机制(Service Provider Interface),其实就是一种将服务接口与服务实现分离以达到解耦、提高可扩展性的机制。SPI 中接口和接口的实现并不在一个项目中,可以说,SPI 机制是项目级别的隔离,这种方式在框架的设计中很常见。Spring 中 SPI 是通过扩展 META-INF/spring.factories 实现的,Dubbo 中也用了类似的方法,扩展了 META-INF/dubbo 等文件。

ApplicationContextInitializer 除了通过 SPI 进行注册外,其实还可以通过硬编码的方式进行注册。那就是修改 Spring Boot 的启动方法,手动添加一个 Initializer。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(DemoApplication.class);
springApplication.addInitializers(new DemoInitializer());
springApplication.run(args);
}

}

这种方式实现的效果和第一种是一样的,本质都是在 SpringApplication 的 initializers 对象上增加一个 Initializer 实例。但是,第一种方式是优于第二种方式的,使用 SPI 的扩展方式,不需要改动原先的代码就可以实现扩展,符合开闭原则。

还有一种方式需要借助 Spring Boot 的配置文件 application.properties。这种方式的原理我们在后面分析 Spring Boot 内置的初始化器的时候会谈到。

1
java复制代码context.initializer.classes=xxx.xxx.DemoInitializer

ApplicationContextInitializer 执行阶段

Spring Boot 执行 main 方法,其实就是执行 SpringApplication 的 run 方法。

1
2
3
java复制代码public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

run 方法是 SpringApplication 的静态方法,其中会生成 SpringApplication 实例对象,真正执行的是实例对象的 run 方法。SpringFactoriesLoader 加载 ApplicationContextInitializer 的过程就发生在生成 SpringApplication 实例的过程中。 类加载完毕,且生成了实例,那这些初始化器什么时候生效呢?如下是 run 方法执行流程。

ApplicationContextInitializer 是在准备 Application 的上下文阶段被执行的。我们知道,spring 是在刷新上下文的时候开始通过 BeanFactory 加载 Bean,所以,ApplicationContextInitializer 的执行发生在 Bean 加载之前,但是此时的 Environment 已经初始化完毕,我们可以在该阶段获得 Environment 的实例,方便增加或修改一些值;此时 ApplicationContext 实例也创建好了,可以预先在上下文中加入一些监听器,处理器等。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);

// 执行 ApplicationContextInitializer
applyInitializers(context);

listeners.contextPrepared(context);
bootstrapContext.close(context);
...
}

applyInitializers 方法中,会遍历之前注册的 initializers,依次调用 initialize 方法。

1
2
3
4
5
6
7
js复制代码protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
...
// 执行 initialize 方法
initializer.initialize(context);
}
}

Spring Boot 内置的初始化器

Spring 提供了扩展 ApplicationContextInitializer 的方法,Spring Boot 将其发扬光大了。我们可以在 spring-boot 的 jar 包下的 META-INF 中找到 spring.factories,如下是其中的 ApplicationContextInitializer 的配置。

1
2
3
4
5
6
7
java复制代码# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,
org.springframework.boot.context.ContextIdApplicationContextInitializer,
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

ConfigurationWarningsApplicationContextInitializer 用于报告 Spring 容器的一些常见的错误配置,可以看出,该初始化器为 context 增加了一个 Bean 的后置处理器。这个处理器是在注册 BeanDefinition 实例之后生效的,用于处理注册实例过程中产生的告警信息,其实就是通过日志打印出告警信息。

1
2
3
4
5
6
7
8
9
java复制代码public class ConfigurationWarningsApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
...
@Override
public void initialize(ConfigurableApplicationContext context) {
context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
}
...
}

ContextIdApplicationContextInitializer 用于设置 Spring 应用上下文 ID,这个 ID 可以通过 ApplicationContext#getId() 的方式获得。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class ContextIdApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
...
@Override
public void initialize(ConfigurableApplicationContext context) {
ContextId contextId = getContextId(applicationContext);
applicationContext.setId(contextId.getId());
applicationContext.getBeanFactory().registerSingleton(ContextId.class.getName(), contextId);
}
...
}

DelegatingApplicationContextInitializer 看到 Delegating 就知道了,这个初始化器是为了别人服务的。这个初始化器会获得 application.properties 下的配置为 context.initializer.classes 的值,这个值是初始化器的全路径名,多个之间用逗号隔开。获得名称后,使用反射将其实例化,并依次触发初始化器的 initialize 方法。DelegatingApplicationContextInitializer 使得 Spring Boot 的用户可以在 application.properties 中配置初始化器。需要注意的是,DelegatingApplicationContextInitializer 的优先级是 0 ,所以不论 context.initializer.classes 配置的初始化器的 order 是多少,都会按照 0 的优先级执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class DelegatingApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private int order = 0;
...
@Override
public void initialize(ConfigurableApplicationContext context) {
// environment 中存放了 Spring Boot 的配合,包括 application.properties 下的配置
ConfigurableEnvironment environment = context.getEnvironment();
// 从 application.properties 中找出配置为 context.initializer.classes 的值
// context.initializer.classes 的值是初始化器的全路径名。可以有多个。
List<Class<?>> initializerClasses = getInitializerClasses(environment);
// 依次触发初始化器
if (!initializerClasses.isEmpty()) {
applyInitializerClasses(context, initializerClasses);
}
}
...
}

RSocketPortInfoApplicationContextInitializer 和 ServerPortInfoApplicationContextInitializer 都是给 ApplicationContext 增加了个监听器,二者都是监听 RSocketServerInitializedEvent 事件,为环境 Environment 中添加一个属性源,不同之处在于一个是增加 SocketPort,一个是增加 ServerPort。代码就不贴了。

除了 spring-boot 下的 META-INF/spring.factories 存在初始化器外,spring-boot-autoconfigure 下也存在 META-INF/spring.factories。这里也定义了两个初始化器。从这里也可以看出,使用 SPI 的方式确实降低了项目间的耦合,每个项目都能定义自己的实现。

1
2
3
4
java复制代码# Initializers
org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

总结

  1. ApplicationContextInitializer 是 Spring 对外提供的扩展点之一,用于在 ApplicationContext 容器加载 Bean 之前对当前的上下文进行配置。
  2. ApplicationContextInitializer 的实现有三种,第一种是在 classpath 路径下的 META-INF/spring.factories 文件中填写接口和实现类的全名,多个实现的话用逗号分隔。第二种是在 Spring Boot 启动代码中手动添加初始化器,第三种是在 application.properties 中配置 context.initializer.classes。
  3. SpringFactoriesLoader 是 spring 提供的,用于加载外部项目配置的加载器。他会固定的读取 META-INF/spring.factories 文件,解析该文件,获得指定接口的实现类。SpringFactoriesLoader 这种加载配置的方式是典型的 SPI 方式,在 Spring Boot 中大量使用,这种方式将服务接口与服务实现分离,达到解耦、提高可扩展性的目的。
  4. Spring Boot 内置了一些初始化器,大部分功能是配置环境变量,比如 ServerPortInfoApplicationContextInitializer,实现手段是为 ApplicationContext 增加监听器。还用于配置日志,比如 ConfigurationWarningsApplicationContextInitializer 实现手段是增加 Bean 后处理器做校验。比较特殊的是 DelegatingApplicationContextInitializer,它会获得 application.properties 中配置的 context.initializer.classes,将其作为初始化器进行加载和执行。

如果您觉得有所收获,就请点个赞吧!

本文转载自: 掘金

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

作为一个后端开发,你需要了解多少Nginx的知识? (一)概

发表于 2021-01-17

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)概述

最近做的一个项目在迁移公网环境的时候出了一个问题,明明服务都起来了,但是怎样都访问不进来。后来才发现是Nginx的配置出了问题。无奈自己关于Nginx的学习在大学毕业后就差不多遗忘了,当时又紧急找不到运维人员,差点就只能以失败结束迁移。

因此觉得就算是干后端开发,对于基本的nginx配置依旧需要有所了解,于是写了这篇后端人员应该懂的nginx文章,希望对大家有所帮助。

(二)Nginx入门

nginx是一个高性能的WEB服务器,Nginx可以实现正向代理、反向代理、负载均衡等功能。比如我现在正在做的一个项目有11台应用服务器,就是用nginx做的反向代理。

关于正向代理和反向代理,两者在概念上有区别,但是在技术实现上完全没有区别。

正向代理:代理服务器放在客户端和服务都能连接到的位置,我们通过访问代理服务器从而访问到目标服务。

反向代理:通过访问代理服务器,代理服务器将请求分发给其他服务,这就是反向代理。

但是在技术上都只需要配置location中的proxy_pass即可。

2.1 安装

这里的安装都在Linux环境下进行,nginx虽然也有window版本,但是还没有见过哪个项目把nginx放在windows服务器上的。

运行nginx需要一些环境,这里需要先安装完毕:

1
bash复制代码yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel pcre pcre-devel

接着下载nginx的压缩包,版本视情况而定,这里用的是1.14.2:

1
bash复制代码wget http://nginx.org/download/nginx-1.14.2.tar.gz

采用默认的方式安装:

1
2
3
bash复制代码./configure
make
make install

分别执行完成之后,nginx运行文件就会安装到/usr/local/nginx中

通过下面的代码检查是否成功:

1
bash复制代码/usr/local/nginx/sbin/nginx -V

2.2 基本命令

1
2
3
4
5
6
7
8
9
10
11
12
powershell复制代码#配置文件启动
./sbin/nginx -c /conf/nginx.conf

#指定nginx程序目录启动
./sbin/nginx -p /usr/local/nginx

#停止:第二种更加优雅
./sbin/nginx -s stop
./sbin/nginx -s quit

#重载配置文件
./sbin/nginx -s reload

2.3 代理的相关参数

1
2
3
4
5
6
7
8
9
10
11
12
powershell复制代码proxy_pass            #代理服务
proxy_redirect off; #是否允许重定向
proxy_set_header Host $host; #转发时传header参数
proxy_set_header X-Forwarded-For $remote_addr; #设置请求的ip地址
proxy_connect_timeout 90; #连接代理服务超时时间
proxy_send_timeout 90; #请求发送最大时间
proxy_read_timeout 90; #读取最大时间
#缓存相关
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;

2.4 设置代理

1
2
3
powershell复制代码location /javayz/ {
proxy_pass http://127.0.0.1:8080;
}

设置完之后当访问 /javayz时就会代理到本机的8080端口上。

proxy_pass后的url是否加/也有讲究,如果加了/则代表绝对根路径,不带就是相对根路径,比如我访问
http://127.0.0.1/javayz/index.html,
按照上面的配置会转发到:
http://127.0.0.1:8080/javayz/index.html。
如果配置改成这样:

1
2
3
powershell复制代码location /javayz/ {
proxy_pass http://127.0.0.1:8080/;
}

转发后的地址变成:http://127.0.0.1:8080/index.html

(三)nginx负载均衡

nginx用的最多的就是负载均衡,通过upstream就能实现负载均衡。

首先我在服务器上部署了两个很简单的SpringBoot项目,其中一个访问8081端口时返回8081,另一个访问8082端口时返回8082

1
2
3
4
5
6
7
java复制代码@RestController
public class IndexController {
@RequestMapping(value = "/",method = RequestMethod.GET)
public String index(){
return "8081";
}
}

接着配置nginx的负载均衡:

1
2
3
4
5
6
7
8
9
bash复制代码#配置集群,这段代码写在http块中
upstream backend {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
#配置在server块中
location / {
proxy_pass http://backend/;
}

接着通过curl命令进行访问:

在这里插入图片描述

8081和8082以轮询的方式运行,如果遇到一台服务器的配置比较好,希望负载的时候更多请求打在那台服务器上,可以给不同的服务增加比重:

1
2
3
4
bash复制代码upstream backend {
server 127.0.0.1:8081 weight=2;
server 127.0.0.1:8082 weight=1;
}

再次执行curl命令:

在这里插入图片描述

upstream的相关参数如下:

1
2
3
4
5
6
7
bash复制代码server 服务ip:端口
weight 权重
max_fails 最多失败连接的次数,超过就认为主机挂掉了
fail_timeout 重新连接的时间
backup 备用服务
max_conns 允许的最大连接数
slow_start 节点恢复后,等待多少秒后再加入

(四)Nginx负载均衡算法

Nginx采用的默认负载均衡算法是轮询+权重,也就是按照设置的权重逐个轮询。除此之外,还有许多其他的负载均衡算法。

ip_hash算法:对于访问的ip,他会做一次hash运算,并对当前的负载应用数量做一次取余运算,这种算法能保证同一个ip访问的是同一台应用服务器。

1
2
3
4
5
bash复制代码upstream backend {
ip_hash;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}

url_hash算法:对于请求的url进行hash运算,这种算法能保证同一个url访问的是同一台应用服务器。

1
2
3
4
5
bash复制代码upstream backend {
url_hash;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}

least_conn算法:将请求分发到连接数最少的节点上。

least_time算法:将请求分配到响应最快的节点上。

(五)Nginx缓存

如果每次请求都需要反向代理给应用服务器,那对带宽和性能的压力是很大的,Nginx中有对缓存的支持,它可以将那些变化不大的前端静态页面加载到缓存中,增强整体的性能。

5.1 在http元素下添加缓存声明

该语句放在http元素下

1
2
3
4
5
6
7
bash复制代码proxy_cache_path /www/javayz/cache levels=1:2 keys_zone=cache_javayz:500m inactive=20d max_size=1g;

#proxy_cache_path 缓存存放的路径
#levels 缓存层级及目录的位数,1:2表示两级目录,第一级目录用1位16进制表示,第二级目录用2位16进制表示
#keys_zone 缓存区内存大小
#inactive 有效期,如果缓存有效期内未使用,则删除
#max_size 存储缓存的硬盘大小

5.2 在location中设定缓存策略

该语句放在location元素中

1
2
3
4
5
6
7
8
bash复制代码#指定缓存区,就是上面设置的key_zone
proxy_cache cache_javayz;

#缓存的key,这里用请求的全路径md5做为key
proxy_cache_key $host$uri$is_args$args;

#对不通的http状态码设置不同的缓存时间,下面的配置表示200时才进行缓存,缓存时间12小时
proxy_cache_valid 200 12h;

5.3 访问生成缓存

可能会出现缓存生成不了的情况,这时候把user切换成root
在这里插入图片描述

访问一次后缓存文件就在上面的路径下生成了。

5.4 缓存的清除

下载模块:

1
bash复制代码wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz

解压

1
bash复制代码tar -zxvf ngx_cache_purge-2.3.tar.gz

在nginx的安装目录下配置

1
bash复制代码./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --add-module=../ngx_cache_purge-2.3

重新编译:

1
bash复制代码make

编译好后会在nginx安装目录下多出一个/objs目录,该目录下有个nginx文件,将这个文件替换到/usr/local/nginx/sbin/中。

检查是否安装成功

在这里插入图片描述

重新运行后,只需要访问http://ip:80/clear/,即可清除缓存

在这里插入图片描述

(六)总结

如果你的公司职责区分严格,那么对于后端开发来说知道nginx的这些概念足够理解整个架构的运行流程了。如果你同时身兼开发和运维的工作,那么这些还不够。学习总是需要不断向前,我们下期再见。

本文转载自: 掘金

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

必须掌握,二叉树的前中后序遍历(迭代+递归)详细代码

发表于 2021-01-17

对于二叉树的三种遍历方式,无论是简单的递归写法,还是迭代写法,都是在面试中容易被考到的,所以这篇文章就把这个常考知识点给解释清楚。

1.二叉树的前序遍历(LeetCode 144题)

前序遍历的关键在于:先遍历根节点,再遍历左子树,再遍历右子树。

即:根→左→右

(1) 递归写法

对于递归写法大家肯定都是非常清楚的,因为它的代码很简单,也比较容易理解,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null) return res;
dfs(root);
return res;
}

public void dfs(TreeNode root){
if(root == null) return ;
res.add(root.val); //先遍历根节点
dfs(root.left); //再遍历左子树
dfs(root.right); //再遍历右子树
}
}

(2) 迭代写法

要把递归写法改成迭代写法,需要用到的一个很重要的数据结构:栈,用它来保存我们上一个结点,也就是记录我们从哪里来,这样在处理完某个结点的时候,我们可以通过栈来倒退回上一步,这就是迭代写法的核心。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stk = new Stack<>();
if(root == null) return res;
while(root != null || !stk.isEmpty()){
while(root != null){
res.add(root.val); //遍历根节点
stk.push(root); //把根节点加入栈中保证我们可以退回到上一步
root = root.left; //遍历左子树
}
root = stk.pop(); //出循环时root为null,回到上一步(即栈顶元素)
root = root.right; //遍历它的右子树
}
return res;
}
}

2.二叉树的中序遍历(LeetCode 94题)

中序遍历的关键在于:先遍历左子树,再遍历根节点,最后遍历右子树。

即:左→根→右。

(1) 递归写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return res;
dfs(root);
return res;
}
public void dfs(TreeNode root){
if(root == null) return ;
dfs(root.left); //遍历左子树
res.add(root.val); //遍历根节点
dfs(root.right); //遍历右子树
}
}

(2) 迭代写法

同样的道理用栈来记录我们的遍历路径,代码与前序遍历十分相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stk = new Stack<>();
if(root == null) return res;
while(root != null || !stk.isEmpty()){
while(root != null){
stk.push(root); //记录遍历路径
root = root.left; //遍历左子树
}
root = stk.pop(); //出循环时root为null,回到上一步(即栈顶)
res.add(root.val); //遍历根节点
root = root.right; //遍历右子树
}
return res;
}
}

对比前序遍历可以发现,其实就是res.add(root.val)这一行代码的位置发生了变化,所以是比较好记忆的。

3.二叉树的后序遍历(LeetCode 145题)

后序遍历的关键在于:先遍历左子树,再遍历右子树,最后遍历根节点。

即:左→右→根。

(1) 递归写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return res;
dfs(root);
return res;
}

public void dfs(TreeNode root){
if(root == null) return ;
dfs(root.left); //遍历左子树
dfs(root.right); //遍历右子树
res.add(root.val); //遍历根节点
}
}

(2) 迭代写法

后序遍历的迭代写法稍微有一点不同,需要转换一下思路,如果我们直接去写的话会发现并不好写,所以我们可以观察一下特点,前序遍历的顺序是:根左右,后序遍历的顺序是:左右根,如果把前序遍历的结果反转一下就是:右左根,和后序遍历的顺序差在左右子树的遍历顺序上,所以后序遍历的迭代写法可以在前序遍历的迭代写法上进行小小的修改。

即:先遍历根节点,再遍历右子树,最后遍历左子树。得到的结果最后反转一下,就是二叉树的后序遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stk = new Stack<>();
if(root == null) return res;
while(root != null ||!stk.isEmpty()){
while(root != null){
res.add(root.val); //遍历根节点
stk.push(root); //记录遍历路径
root = root.right; //遍历右子树
}
root = stk.pop(); //出循环时root为null,回到上一步(即栈顶)
root = root.left; //遍历左子树
}
Collections.reverse(res); //反转结果
return res;
}
}

总结

我们可以发现,其实二叉树的前中后序遍历的迭代写法是非常相似的,我们只需要理解之后就十分容易直接记住。

前序遍历与中序遍历代码的区别只是一行代码的位置发生了改变;

后序遍历是在前序遍历的基础上稍作修改;

所以我们只需要记住三点:

(1) 迭代写法需要用到栈

(2) 循环是while(root != null || !stk.isEmpty())

(3) 前序遍历思路,中序遍历改一行代码位置,后序遍历:根右左最后把答案反转

二叉树的遍历就是这么简单~

本文转载自: 掘金

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

PHP本地环境不会搭建,看这里!

发表于 2021-01-17

前言

2021年第一篇,最近想用 PHP 搞个项目玩玩,但万万没想到倒在了环境搭建这一步。

首先在网上冲浪一番后,你会发现PHP的搭建方式其实挺多(但有时候选择多并不是一件好事),目前常见的搭建方式有:Homestead(基于虚拟机)、XAMPP、MAMP、Laradock(基于Docker)等,但这些都比较重量级;轻量级的也有,比如 Valet,但之前试过 Valet 发现它会修改本地配置,典型如Nginx,而且在卸载的时候还不小心把Nginx配置全删了,顿时觉得不太可控,所以最后选择的方案是 Nginx + php-fpm,足够可用,同时也不会太重量级。

注:本文基于 Mac 环境。

安装流程

  • 1.检查 php 是否安装
  • 2.检查 composer 是否安装
  • 3.检查 laravel 是否安装
  • 4.检查 php-fpm 是否安装
  • 5.检查 nginx 是否安装
  • 6.设置多版本共存
  • 7.创建 laravel 项目并测试

检查php是否安装

现在比较新的 Mac 系统都会自带php以及php-fpm,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码# 版本
$ php -v
PHP 7.3.11 (cli) (built: Jun 5 2020 23:50:40) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies

# 执行路径
$ which php
/usr/bin/php

# 配置文件
$ ls /etc/php-fpm.conf
/etc/php-fpm.conf

# 实际的配置文件,因为 /etc/php-fpm.conf 使用了include关键字
$ ls /private/etc/php-fpm.d
www.conf www.conf.default

检查composer是否安装

composer 是php的包管理器,类似 pip,这个Mac系统就没有自带,需要自己安装。

1
2
3
4
5
shell复制代码# 安装
$ brew install composer

$ composer --version
Composer version 1.9.0 2019-08-02 20:55:32

检查laravel是否安装

这个不是非必须,主要是后面会创建laravel项目,来测试php环境是否搭建成功,建议安装,后续你也会用到。

1
2
3
4
5
6
shell复制代码# 安装
$ composer global require laravel/installer

# 测试
$ laravel --version
Laravel Installer 4.1.1

检查nginx是否安装

由于我搭建php环境选择的方案是 nginx + php-fpm,所以这里必须安装nginx(其实不管啥方案都会用到nginx,区别是自己安装,还是别人帮你安装好)。

1
2
3
4
5
6
shell复制代码# 安装
$ brew install nginx

# 测试
$ nginx -v
nginx version: nginx/1.19.5

多版本共存

由于历史原因,很多项目的php版本并没有一个统一的标准,项目版本可能会在php5.6、php7.2,甚至php8.0之间跳跃,而版本的不一致则带来语法的不一致,导致出现一些奇怪的问题,所以多版本共存就显得非常重要。

本次通过homebrew实现多版本并存,实现起来也比较简单,首先我们弃用Mac自带的php,全部使用brew安装的。

安装php5.6

1
2
3
4
5
6
shell复制代码# 安装
$ brew install php@5.6

# 配置环境变量 (可通过 brew info php@5.6 查看)
echo 'export PATH="/usr/local/opt/php@5.6/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="/usr/local/opt/php@5.6/sbin:$PATH"' >> ~/.zshrc

安装php7.2

1
2
3
4
5
6
shell复制代码# 安装
$ brew install php@7.2

# 配置环境变量 (可通过 brew info php@7.2 查看)
echo 'export PATH="/usr/local/opt/php@7.2/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="/usr/local/opt/php@7.2/sbin:$PATH"' >> ~/.zshrc

php-fpm 配置不同端口

多版本共存的核心是启动不同版本的 php-fpm,所以就要避免端口占用。

为啥需要 php-fpm 呢?这也是我一开始疑惑的地方,这是因为PHP不像Javascript,可以直接被浏览器解析,所以当用户请求PHP程序时,会先经过Nginx,Nginx会通过php-fpm将请求转发到php解释器,并进行处理,具体可以参考这里

1
2
3
4
5
6
7
8
9
shell复制代码# 配置文件路径可通过 brew info php@5.6 或者 brew info php@7.2 得到

# php5.6 监听9001端口
$ vim /usr/local/etc/php/5.6/php-fpm.conf
listen = 127.0.0.1:9001

# php7.2 监听9002端口
$ vim /usr/local/etc/php/7.2/php-fpm.d/www.conf
listen = 127.0.0.1:9002

启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码# 启动php5.6
brew services start shivammathur/php/php@5.6

# 启动php7.2
brew services start php@7.2

# 查看
$ ps -ef | grep php-fpm
501 12981 1 0 11:35上午 ?? 0:00.15 /usr/local/opt/php@5.6/sbin/php-fpm --nodaemonize
501 12988 12981 0 11:35上午 ?? 0:00.74 /usr/local/opt/php@5.6/sbin/php-fpm --nodaemonize
501 12989 12981 0 11:35上午 ?? 0:00.00 /usr/local/opt/php@5.6/sbin/php-fpm --nodaemonize
501 13141 1 0 11:35上午 ?? 0:00.14 /usr/local/opt/php@7.2/sbin/php-fpm --nodaemonize
501 13148 13141 0 11:35上午 ?? 0:00.17 /usr/local/opt/php@7.2/sbin/php-fpm --nodaemonize
501 13149 13141 0 11:35上午 ?? 0:00.00 /usr/local/opt/php@7.2/sbin/php-fpm --nodaemonize

此时php多版本搭建完毕,通过后面的测试可以查看搭建是否成功。

项目部署及测试

本次我们会通过部署两个版本的laravel项目,从而测试上述多版本的正确性,以及让朋友们知道如何部署php项目。

1.创建项目

1
2
3
4
5
6
shell复制代码# 创建项目
# laravel 5.2 要求的php版本最低是 5.5.9
composer create-project laravel/laravel=5.2.* php56 --prefer-dist

# laravel 5.6 要求的php版本最低是 7.1.3
composer create-project laravel/laravel=5.6.* php72 --prefer-dist

2.创建 blog56 的配置

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
ini复制代码vim /usr/local/etc/nginx/servers/php56.conf

server {
# 监听的端口,也就是我们访问的端口
listen 6001 default_server;
server_name 127.0.0.1;

# 默认网站根目录(www目录)
root /Users/xxx/PhpstormProjects/php56/public;
access_log /Users/xxx/PhpstormProjects/php56/access.log;
error_log /Users/xxx/PhpstormProjects/php56/error.log;

location / {
try_files $uri /index.php?$query_string;
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}

# PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI协议默认配置.
location ~ \.php$ {
# 这个是我们上述设置的端口,9001会交由 php5.6 处理
fastcgi_pass 127.0.0.1:9001;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

3.创建 blog72 的配置

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
ini复制代码vim /usr/local/etc/nginx/servers/php72.conf

server {
# 监听的端口,也就是我们访问的端口
listen 6002 default_server;
server_name 127.0.0.1;

# 默认网站根目录(www目录)
root /Users/xxx/PhpstormProjects/php72/public;
access_log /Users/xxx/PhpstormProjects/php72/access.log;
error_log /Users/xxx/PhpstormProjects/php72/error.log;

location / {
try_files $uri /index.php?$query_string;
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}

# PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI协议默认配置.
location ~ \.php$ {
# 这个是我们上述设置的端口,9002会交由 php7.2 处理
fastcgi_pass 127.0.0.1:9002;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

4.访问测试: 浏览器访问 http://localhost:6001/ 以及 http://localhost:6002/ 可以看到欢迎界面

踩坑总结

1.Nginx配置一定要加上 access_log 和 error_log,这样出错的时候才能定位到,因为很多情况下都是Nginx配置出问题!!!

2.注意浏览器代理问题,很多人包括我习惯性在浏览器测试(GET请求),但有时候由于代理问题,请求会失败,这时可尝试在命令行,或者其他浏览器执行(个人经验)

3.之前一开始配置时参考过一篇文章[《Laravel 项目部署,php-fpm 及 nginx 配置》],但是配置后会出现”访问直接下载index.php文件“的情况,原因是该文的Nginx配置有点问题:

1
2
3
4
5
6
7
8
9
shell复制代码# 作者的配置(不能说他配置有问题,只能说大家环境不同)
location / {
try_files $uri $uri/ /index.php?$query_string;
}

# 实际的配置
location / {
try_files $uri /index.php?$query_string;
}

解决方案参考:stackoverflow.com/questions/1…

提醒:这里要给个提醒,很多人由于配置不正确(权限或者路径问题),导致访问失败,所以切记一定要加上 access_log 和 error_log,才能知道报了什么错!!另外,有时候可能由于代理问题,在浏览器直接测试可能会不成功。

参考

1.PHP安装配置篇-学院君

2.mac中利用brew实现多版本php共存以及任意切换

3.Mac下启动php-fpm问题解决

本文转载自: 掘金

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

初识MyBatis Plus 快速入门

发表于 2021-01-16

前言

C: 在 Java Web 的日常开发中,风靡中日韩的持久层框架 MyBatis ,想必你不会陌生。如果你不认识它,那么本篇目前不适合你,请先学习 《初识 MyBatis 系列》 后再过来。

MyBatis 框架,作为一款非常优秀的 半自动的持久层ORM框架 。它支持自定义 SQL、动态SQL、存储过程以及高级映射, 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作 。

它的确很好,但是你也再看看下方代码。在 MyBatis 中,不同实体的基础数据操作几乎属于套模板一样。尤其是在项目搭建初期,要写一大堆的基础 CRUD。随着开发工作量及工作时间上升,这就又成了天下程序员苦之久矣的事儿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public interface UserMapper {
// 增加
int insert(User user);
// 修改
int update(User user);
// 删除
int deleteById(@Param("id") Long id);
// 根据ID查询
User selectById(@Param("id") Long id);
// 根据条件查询总记录数
Integer selectCount(Map<String, Object> params);
// 根据条件查询列表
List<User> selectByMap(Map<String, Object> params);
}

当然,MyBatis 官方在一开始就想到了这事儿,所以提供了一套代码生成器。其他还有一些第三方 IDE 插件也有类似功能,但都到今天这年头了,你看看最近的一篇 MyBatis 代码生成器讲解文章下的评论。

image-20210116170043541

类似的评论在类似的文章中,不知凡几。我们自然是要跟随上技术时代的浪潮,做技术的 “弄潮儿”,接下来查老师会开启一个新的系列《初识MyBatis Plus》,来讲解体验下这个人云亦云的 MyBatis Plus。

时尚弄潮儿

简介

MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具, 在 MyBatis 的基础上只做增强不做改变 ,为简化开发、提高效率而生。[1]

img

顾名思义,MyBatis Plus 是 MyBatis 的 Plus 版本,也就是升级版、增强版的意思。

查老师有话说: 由于名字较长,后面查老师会较多的叫它的简称:MP,注意是 MP 不是 MMP。

特点

以 MP 的作者所言:MyBatis Plus 的愿景是成为 MyBatis 最好的搭档,就像魂斗罗中的 1P、2P,基友搭配,效率翻倍。[1]

看看下方总结的 MyBatis Plus 特点,有没有一些心动。

  • 无侵入 :只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小 :启动即会自动注入基本 CRUD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作 :内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用 :通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成 :支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式 :支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作 :支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器 :采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置
  • 内置分页插件 :基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库 :支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件 :可输出 SQL语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件 :提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

使用步骤

看官方说的这么好,心动不如行动,赶快跟着查老师来体验一下吧。

我们将通过一个简单的 Demo 来体验 MP 的强大功能,在此之前,查老师假设你已经:

  • 拥有 Java 开发环境 以及相应 IDE(IDEA)
  • 熟悉 MySQL 数据库
  • 熟悉 Maven 工具
  • 熟悉 Lombok 工具
  • 熟悉 MyBatis 技术
  • 熟悉 Spring Boot 技术

数据库准备

首先我们需要准备好一个数据库,并添加点测试数据。

下方是我们的测试数据:

主键 姓名 年龄 邮箱
1 Jone 18 Jone@126.com
2 Jack 20 Jack@126.com
3 Tom 28 Tom@126.com
4 Sandy 21 Sandy@126.com
5 Billie 24 Billie@126.com

其对应的数据库 结构 脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码-- 创建并切换数据库
CREATE DATABASE mybatisplus_demodb;
USE mybatisplus_demodb;

-- 创建用户数据表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;

其对应的数据库 数据 脚本如下:

1
2
3
4
5
6
7
8
9
sql复制代码-- 清空用户表数据
TRUNCATE TABLE user;

-- 向用户表插入测试数据
INSERT INTO `user` VALUES (1, 'Jone', 18, 'Jone@126.com');
INSERT INTO `user` VALUES (2, 'Jack', 20, 'Jack@126.com');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'Tom@126.com');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'Sandy@126.com');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'Billie@126.com');

依次执行完结构及数据脚本后,最终数据库效果如下:

image-20210123141306971

创建项目

在有了 Spring Boot 之后,MyBatis 开发变得非常简单。作为 MyBatis 的升级版,毫无疑问,MP 的开发团队也为 Spring Boot 开发准备了一份 starter,这让我们开发一个 MP 项目变得 Easy。

我们使用 IntelliJ IDEA 中封装的 Spring Initializer 来快速初始化一个 Spring Boot 工程。

image-20210123142555612

填写好 Maven 的 GAV 信息。

image-20210123141820751

在项目构建选择依赖这一步的时候,直接选择好 Lombok 和 MySQL 驱动的依赖。

image-20210123141949195

构建好的项目结构如下。

image-20210123142352571

最后,我们需要调整下 Spring Boot 的版本,以及导入好 MP 的 starter 依赖。

image-20210123143251553

查老师有话说: 为了方便使用本案例中的单元测试,建议采用 2.2.x 以上的 Spring Boot。

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
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--
Spring Boot已经做好了相应依赖版本的锁定工作。
调整一下Spring Boot版本,Lombok和MySQL驱动的版本就会随之变动。
-->
<version>2.2.11.RELEASE</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>mybatis-plus-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- Spring Boot基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot单元测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- MySQL数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!-- MyBatis Plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>

查老师有话说: 引入 MP 依赖之后,就不要再引入 MyBatis 以及 MyBatis-Spring 这些依赖了,因为 MP 都包含好了。你再引入,还可能出现因版本差异导致的其他问题。

image-20210123143508079

创建POJO类

项目创建好后,按原来 MyBatis 的使用步骤,首先要根据数据库编写好 POJO 类。

1
2
3
4
5
6
7
8
9
java复制代码@Data
public class User {

private Long id;
private String name;
private Integer age;
private String email;

}

查老师有话说: 此处使用了 Lombok 简化 getter/setter 等代码,如果不会用,可以去学习《初识Lombok》 或者直接自己手写 getter/setter 等代码。

创建Mapper接口

有了 POJO 类之后,原来的 MyBatis 中,下一步自然就要写 Mapper 接口,写 SQL 映射文件了。但是现在你只需要写好 Mapper 接口,然后继承一个由 MP 所提供的 BaseMapper 接口即可。

1
2
3
4
java复制代码// 注:给 BaseMapper 指定好泛型,它里面的 CRUD 需要使用泛型来确定具体操作数据类型
public interface UserMapper extends BaseMapper<User> {

}

查老师有话说: 在 BaseMapper 中封装有大量的基础 CRUD 操作,这一点就直接解决了我们前言中所提到的需求。

添加配置

写好核心部分代码后,MP 需要像 MyBatis 整合 Spring Boot 一样,在 application.yml 配置文件中添加上数据源配置及一些 MyBatis 自定义配置。

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///mybatisplus_demodb?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
username: root
password: root
mybatis-plus:
configuration:
# 控制台打印SQL日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

查老师有话说: 一般我们还会给 MyBatis 添加些自定义配置:类型别名包扫描,指定 Mapper 映射文件地址等。这回这些配置都被 MP 整合在一起了,有的还拥有了默认值。

例如:原来指定 Mapper 映射文件地址需要用:mybatis.mapper-locations ,现在 MP 中需要用:mybatis-plus.mapper-locations,MP 还给它准备了默认值:classpath*:/mapper/**/*.xml。

这意味着啥?意味着,这种配置以后如果没有额外指定,就不需要我们再配置它啦。

image-20210122174737497

最后, MP 同样也需要在 Spring Boot 启动类中,添加 @MapperScan 注解来指定 Mapper 接口扫描包。

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@MapperScan("com.example.mapper")
public class MybatisPlusDemoApplication {

public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}

}

测试

终于搞定了,我们再去测试类中编写一个测试用例,去测试一下效果就完事儿了。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
class MybatisPlusDemoApplicationTests {

@Autowired
private UserMapper userMapper;

@Test
void testSelectList() {
// 查询用户列表
List<User> userList = userMapper.selectList(null);
// 遍历用户列表
userList.forEach(System.out::println);
}

}

MP快速入门单元测试

控制台输出:

1
2
3
4
5
6
7
8
9
sql复制代码==>  Preparing: SELECT id,name,age,email FROM user
==> Parameters:
<== Columns: id, name, age, email
<== Row: 1, Jone, 18, Jone@126.com
<== Row: 2, Jack, 20, Jack@126.com
<== Row: 3, Tom, 28, Tom@126.com
<== Row: 4, Sandy, 21, Sandy@126.com
<== Row: 5, Billie, 24, Billie@126.com
<== Total: 5
1
2
3
4
5
less复制代码User(id=1, name=Jone, age=18, email=Jone@126.com)
User(id=2, name=Jack, age=20, email=Jack@126.com)
User(id=3, name=Tom, age=28, email=Tom@126.com)
User(id=4, name=Sandy, age=21, email=Sandy@126.com)
User(id=5, name=Billie, age=24, email=Billie@126.com)

查老师有话说: 你在编写单元测试时,可能会看到如下报错。别担心这不是什么要紧的事儿,不影响你运行,也不影响你开法拉利。

如果你看不惯它,那就去 UserMapper 接口上加一个 @Repository 或 @Component 注解,这问题就迎刃而解了。

image-20210122180134757

参考文献

[1]MyBatis Plus 官网. 指南[EB/OL]. baomidou.com/guide/. 2021-01-16

后记

C: 好了,MP 的快速入门就到这儿结束了,是不是快的都让你觉得在做梦?通过以上几个简单的步骤,我们就拥有了一个具有通用 CRUD 操作功能的 Mapper 接口,甚至连 XML 文件都不用编写!

当然,若是它们还不够你用,或是你需要写一些复杂的数据操作,继续按照 MyBatis 原来的玩法去写就可以了,一点不影响。

MyBatis-Plus 的强大远不止如此,想要详细了解 MyBatis-Plus 的强大功能,那就继续查看查老师该系列的下一篇吧!

另外,提醒一下,本系列的编写思路,就是基于官网而来,甚至有同学看完官网会觉得查老师多余了。但查老师一贯以小白文著称,学起来不吃力,学重点,比官网更细致,是查老师本系列的目的。所以等等再 “恶评” 吧。

我根本没在怕

查老师有话说: 对于技术的学习,查老师一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。

所以在查老师的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。

本文转载自: 掘金

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

Springboot集成Swagger2及常见配置(无坑版)

发表于 2021-01-16

本文出自《愚公要移山》

收录于《Springboot专题》中

这种整合的文章确实已经烂大街了,写他一方面是补充我的springboot系列,另一方面确实还有一部分小伙伴没用过。最重要的是,如果你忘记了这种整合的代码。可以随时查阅。

前言

现在的开发基本上都是前后端分离,前后端交互都是通过API文档。有了API文档大家各自开发,互不干扰。

1、传统方式

传统方式是文档设计好之后,分别发给前端和后端人员。这样有个缺点,接口信息一旦变化,文档就需要重新发送给前后端人员。无法做到实时。所以浪费时间和精力。

2、swagger方式

我们的后台应用集成了swagger之后,会自动暴露出我们的接口,而且这个接口形式还是通过restful风格发布的。一旦后端的接口有变化,会立刻显示出来,因此极大地提高了效率。

OK,基本上一句话就可以总结他的好处,那就是后端写的api文档可以通过swagger的形式实时的发布出来,供前端人员查看。

3、其他方式

swagger的页面说实话长得不好看,也有一些其他的方案,不是有很多bug,就是收费。目前swagger是使用的最多的。我目前也正在做这个样的开源项目,基于swagger做出类似于其他方案的页面,而且功能更加的强大。

一、代码整合

前提条件是要新建一个springboot项目。这点就不演示了。

第一步:添加依赖

1
2
3
4
5
6
7
8
9
10
复制代码<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

2.9.2的版本是用的最多的,具体的可以直接去maven的官网去搜索,找一个使用量最多的版本即可。

第二步:配置

新建config包,创建SwaggerConfig类

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
复制代码@EnableSwagger2
@Configuration
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
             .apiInfo(apiInfo())
             .select()
             //为当前包路径,控制器类包
             .apis(RequestHandlerSelectors.basePackage("com.fdd.controller"))
            .paths(PathSelectors.any())
             .build();
    }
    //构建 api文档的详细信息函数
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            //页面标题
           .title("XX平台API接口文档")
            //创建人
           .contact(new Contact("冯冬冬", "http://www.javachat.cc",  
                 "3049352171@qq.com"))
           //版本号
          .version("1.0")
           //描述
          .description("系统API描述")
          .build();
    }

这里的配置也比较简单。这里有很多选项供我们去配置。如果我们的项目有多个组,只需要创建多个Docket即可。这时候扫描的包换成每个组的包路径。

第三步:controller类中配置

新建一个controller包,然后创建HelloController类

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Api("Hello控制类")
@RestController 
public class HelloController {
    @GetMapping(value = "/user")
    public User getUser(){
        return new User("愚公要移山","123456");
    }
    @ApiOperation("可以指定参数的API")
    @PostMapping("/param")
    public String hello2(@ApiParam("用户名") String name){
        return "hello" + name;
    }
}

这里我们可以看出,使用注解就可以对这个类、方法、字段等等进行解释说明。其他的字段还有很多,在使用的时候会有相应的提示,可以自己试一遍:

第四步:查看效果

访问:http://127.0.0.1:8080/swagger-ui.html即可。


这里就是最终的展示效果。OK,到这一步基本上就集成进来了。下面说一下可能会遇到的配置。

三、常见其他问题

1、Spring Security - 配置免认证访问

有时候我们的Springboot集成了SpringSecurity,这时候如果访问swagger的地址会自动跳转到登录页面。这是因为SpringSecurity对其进行了拦截。为此我们只需要在我们的SpringSecurity配置一下进行放行即可。


现在配置一下,进行放行。在config包下新建一个SpringSecurityConfig类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/v2/*").permitAll()
                .antMatchers("/csrf").permitAll()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;
    }
}

此时就可以正常的访问了。

2、为swagger设置jwt

这种方式比较简单,只需要一步即可。修改我们的swaggerConfig类即可。

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
复制代码@EnableSwagger2
@Configuration
public class Swagger2Config {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .securityContexts(Arrays.asList(securityContext()))
                .securitySchemes(Arrays.asList(apiKey()))
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }
    //构建 api文档的详细信息函数
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //页面标题
                .title("XX平台API接口文档")
                //创建人
                .contact(new Contact("冯冬冬", "http://www.javachat.cc",
                        "3049352171@qq.com"))
                //版本号
                .version("1.0")
                //描述
                .description("系统API描述")
                .build();
    }
    private ApiKey apiKey() {
        return new ApiKey("JWT", "Authorization", "header");
    }
    private SecurityContext securityContext() {
        return SecurityContext.builder().securityReferences(defaultAuth()).build();
    }

    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope 
         = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        return Arrays.asList(new SecurityReference("JWT", authorizationScopes));
    }

}

加了一些token验证的代码,比较简单,关于JWT的东西,可以私下了解。这里不赘述了。

3、隐藏Endpoint

有时候自己写的controller,或者是controller里面的接口方法不想让前端人员看到,我们可以隐藏即可。

第一:隐藏整个controller

1
2
3
4
5
复制代码@ApiIgnore
@RestController
public class MyController {
    //方法
}

第二:隐藏某个接口方法1

1
2
3
4
5
6
复制代码@ApiIgnore
@ApiOperation(value = "描述信息")
@GetMapping("/getAuthor")
public String getAuthor() {
    return "愚公要移山";
}

第三:隐藏某个接口方法2

1
2
3
4
5
复制代码@ApiOperation(value = "描述信息", hidden = true)
@GetMapping("/get")
public LocalDate getDate() {
    return LocalDate.now();
}

OK,很多配置基本上就到这了。后续会继续补充。

本文转载自: 掘金

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

微信公众号开发系列-7、消息管理-接收事件推送

发表于 2021-01-16

1、概述

在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许

我们在上一篇微信C#系列-6、消息管理-普通消息接受处理中讲到,微信的消息可以大体分为两种类型,一种是包括:文本,语音,图片等的普通消息,另一种就是本篇要将的事件类型。包括:关注/取消关注事件,扫描带参数二维码事件,上报地理位置事件,自定义菜单相关事件等,本篇一一进行讲解。介于偏于内容过多易产生阅读疲劳,对于自定义菜单相关事件的处理我们放在下一篇中讲解。

这里的消息指的是传统的微信公众平台消息交互,微信用户向公众号发送消息后,公众号回复消息给微信用户。包括以下类型:

  1. 关注/取消关注事件:subscribe/unsubscribe
  2. 扫描带参数二维码事件:scan
  3. 上报地理位置事件:location
  4. 自定义菜单事件
  5. 点击菜单拉取消息时的事件推送
  6. 点击菜单跳转链接时的事件推送

本篇主要介绍前三种。

2、实现方式

使用Senparc.Weixin框架来快速处理各种接收事件推送,实现非常简单,自定义一个继承MessageHandler的类,重写这些类型的方法即可。注意:DefaultResponseMessage必须重写,用于返回没有处理过的消息类型(也可以用于默认消息,如帮助信息等);其中所有原OnXX的抽象方法已经都改为虚方法,可以不必每个都重写。若不重写,默认返回DefaultResponseMessage方法中的结果。

自定义消息处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码public partial class CustomMessageHandler : MessageHandler<MessageContext<IRequestMessageBase, IResponseMessageBase>>
{
public CustomMessageHandler(Stream inputStream, int maxRecordCount = 0)
: base(inputStream, null, maxRecordCount)
{
WeixinContext.ExpireMinutes = 3;
}
public override void OnExecuting()
{
//测试MessageContext.StorageData
if (CurrentMessageContext.StorageData == null)
{
CurrentMessageContext.StorageData = 0;
}
base.OnExecuting();
}
public override void OnExecuted()
{
base.OnExecuted();
CurrentMessageContext.StorageData = ((int)CurrentMessageContext.StorageData) + 1;
}
}

定义好事件处理类后,分别重写上面提到几种接收事件推送的事件即可。
我们可以通过重写MessageHandler里的这几种类型方法来处理我们的业务,当然也可以只重写需要的部分类型,不需要的类型可以不重写,只需要定义一个统一的DefaultResponseMessage

1
2
3
4
5
6
7
kotlin复制代码public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage)
{
//所有没有被处理的消息会默认返回这里的结果
var responseMessage = this.CreateResponseMessage<ResponseMessageText>();
responseMessage.Content = "这条消息来自DefaultResponseMessage。";
return responseMessage;
}

3、消息的去重的重要性

上一篇我们就已经提到过微信服务器在5秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。如此以来,我们模拟有这样一个场景:当用户关注微信账号时,获取当前用户信息,然后将信息写到数据库中,类似网站的注册。假设这个关注事件中,我们需要处理比较复杂的业务逻辑。如送积分,写用户日志,分配用户组等等一系列的逻辑需要执行,或者网络环境比较复杂,无法保证5秒内响应当前用户的操作,那如果当操作尚未完成,微信服务器又给我们的服务器推送了一条相同的关注事件,我们将再次执行我们的那些逻辑,这样就有可能导致数据库中出现重复的数据(有的童鞋就会说了,我在插入数据之前先判断当前是否已经存在了,如果存在了就不执行插入的操作。我想说的是,我当初也是这样想的,但真实的运行环境和我们的调试环境还是有差距的,直到发现数据库中有不少重复的用户信息时,我才发现消息去重的重要性。)。

消息的去重普通消息和事件消息是有区别的。普通消息使用msgid,而事件消息使用FromUserName + CreateTime。

4、关注/取消关注事件

用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。

假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。

关注或取消事件推送XML数据包示例:

1
2
3
4
5
6
7
xml复制代码<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
</xml>

参数说明:

1
2
3
4
5
6
scss复制代码参数				描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,subscribe(订阅)、unsubscribe(取消订阅)

4.1 关注事件

关注事件我们只需要重写OnEvent_SubscribeRequest事件代码即可,如下我们返回了一个文本消息,实现代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码/// <summary>
/// 订阅(关注)事件
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_SubscribeRequest(RequestMessageEvent_Subscribe requestMessage)
{
var responseMessage = CreateResponseMessage<ResponseMessageNews>();
foreach (var model in messageList)
{
responseMessage.Articles.Add(new Article()
{
Title = "国思公众号",
Description = "欢迎关注国思软件公众号,更多内容稳步官网,多谢!",
PicUrl = "http://www.rdiframework.net/WeiXin.png",
Url = "http://www.rdiframework.net/"
});
}
return responseMessage;
}

关注事件执行后效果

在上面的关注事件中,用户关注公众号就会自动执行上面的事件代码,我们就可以在事件代码中做相关的业务处理,如绑定用户分组、增加用户到本地等等。同时推送一条欢迎消息返回到用户手机上。

4.2 取消关注事件

取消关注事件与关注事件类似,主要是事件变成了unsubscribe(取消关注)。取消关注事件-unsubscribe的主要意义在于及时删除网站应用中已经记录的OpenID绑定,消除冗余数据,并且关注用户流失的情况。

取消关注事件我们只需要重写OnEvent_UnsubscribeRequest事件代码即可,如下我们返回了一个文本消息,实现代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码/// <summary>
/// 退订/取消关注
/// 实际上用户无法收到非订阅账号的消息,所以这里可以随便写。
/// unsubscribe事件的意义在于及时删除网站应用中已经记录的OpenID绑定,消除冗余数据。并且关注用户流失的情况。
/// </summary>
/// <returns></returns>
public override IResponseMessageBase OnEvent_UnsubscribeRequest(RequestMessageEvent_Unsubscribe requestMessage)
{
int returnValue = RDIFrameworkService.Instance.WeixinBasicService.UserUnsubscribeByOpenId(Id,requestMessage.FromUserName);//退
var responseMessage = base.CreateResponseMessage<ResponseMessageText>();
responseMessage.Content = "有空再来";
return responseMessage;
}

上面的代码在用户取消公众号的关注时就会自动执行,可以看到我们有一行代码针对用户取消关注时执行的业务逻辑,同时返回了一个文本消息。实际用户已经取消关注,返回的消息也返回不到用户手机上的。

5、扫描带参数二维码事件

用户扫描带场景值二维码时,可能推送以下两种事件:

  1. 如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。
  2. 如果用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者。

5.1 接口展示与实现方式

对于第一种上面已经讲了,这里就只说明下第二种。

推送XML数据包示例:

1
2
3
4
5
6
7
8
9
xml复制代码<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[SCAN]]></Event>
<EventKey><![CDATA[SCENE_VALUE]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>

参数说明:

1
2
3
4
5
6
7
8
vbnet复制代码参数				描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,SCAN
EventKey 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
Ticket 二维码的ticket,可用来换取二维码图片

对于生成带参数的二维码我们会在后面的文章中专门介绍,这儿我们了解一个这个概念。为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。具体官方技术文档可参考:生成带参数的二维码

目前有2种类型的二维码:

1、临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景

2、永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。

扫描带参数二维码事件只需要重写OnEvent_ScanRequest事件代码即可,如下我们返回了一个文本消息,实现代码参考:

1
2
3
4
5
6
7
8
9
csharp复制代码public override IResponseMessageBase OnEvent_ScanRequest(RequestMessageEvent_Scan requestMessage)
{
//通过扫描关注
var responseMessage = CreateResponseMessage<ResponseMessageText>();

responseMessage.Content = responseMessage.Content ?? string.Format("欢迎关注国思软件,通过扫描二维码进入,场景值:{0}", requestMessage.EventKey);

return responseMessage;
}

在上面的代码中用户扫描了带场景值的二维码进入公众号后我们返回了一个提示的文本消息。这是非常有用的功能,常用途推广,可以根据不同的二维码场景值分别做不同的业务处理,如可以统计关注的每一个粉丝从哪里来的,做到渠道推广分析,但是关注的都是同一个公众号。

5.2 生成带参数的二维码用途

微信公众号生成带参数的二维码有何用途?

  1. 可以区分粉丝来源,只需要生成不同的带参数的二维码,把这些二维码分别投放到各个渠道,粉丝通过这些渠道二维码进来就可以区分粉丝来源,微号帮后台渠道粉丝列表中有粉丝数及明细;
  2. 粉丝通过扫描渠道二维码关注公众号,会打标签分组,比如粉丝扫商店A、B的二维码进来的, 在微信公众号后来的用户管理中可查看到商店A/B二维码名下的粉丝明细及分组情况;
  3. 可以生成多个不同的渠道二维码配置不同的营销活动,设置不同的关注回复信息,让粉丝第一时间了解活动动机,是否有兴趣参与等等;
  4. 可以利用渠道二维码生成功能,可以实现微信收款前关注公众号,间接分析粉丝后续消费情况;
  5. 考核推广员完成任务的进度,如以推广名字生成多不个同的二维码,分配给不同的推广员,每个推广员吸引了多少粉丝关注公众号,微号帮后台都可以一一明细;
  6. 带参数的二维码也叫渠道二维码或者场景二维码,生存的数量有限,且是永久二维码。当数量用完后可以删除一些不用的二维码释放出来,二次利用。

通过扫描带场景值的二维码进入

6、上报地理位置事件

用户同意上报地理位置后,每次进入公众号会话时,都会在进入时上报地理位置,或在进入会话后每5秒上报一次地理位置,公众号可以在公众平台网站中修改以上设置。上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。要获取用户地址位置,需要在微信公众平台开发者中心开启上报地理位置功能,开启之后会在用户首次进入公众号时,弹出是否允许上报地理位置选项,如果选择允许则在用户每次进入公众号会话的时候微信会以XML形式将用户的地理位置上报到你开发者中心填写的URL上。

**注意:**用户地理位置是被动获取的,需用户同意后才会上报,微信公众平台开发不能主动获取用户地理位置。

推送XML数据包示例:

1
2
3
4
5
6
7
8
9
10
xml复制代码<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[LOCATION]]></Event>
<Latitude>23.137466</Latitude>
<Longitude>113.352425</Longitude>
<Precision>119.385040</Precision>
</xml>

参数说明:

1
2
3
4
5
6
7
8
9
vbnet复制代码参数				描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,event
Event 事件类型,LOCATION
Latitude 地理位置纬度
Longitude 地理位置经度
Precision 地理位置精度

上报地理位置事件只需要重写OnEvent_LocationRequest事件代码即可,如下我们返回了一个文本消息,实现代码参考:

1
2
3
4
5
6
7
csharp复制代码public override IResponseMessageBase OnEvent_LocationRequest(RequestMessageEvent_Location requestMessage)
{
//这里是微信客户端(通过微信服务器)自动发送过来的位置信息
var responseMessage = CreateResponseMessage<ResponseMessageText>();
responseMessage.Content = "这里写什么都无所谓,比如:上帝爱你!";
return responseMessage;//这里也可以返回null(需要注意写日志时候null的问题)
}

上报地理位置用处非常多,可以用维度和经度获取城市代号,调用天气Api,也可以用来监测企业员工的位置进行微信考勤。在微信运营的时候,用户地理位置还是我们进行营销策划、广告活动投放、用户精准营销的重要依据。

参考文章

微信公众平台技术文档-官方

Senparc.Weixin SDK + 官网示例源代码

RDIFramework.NET — 基于.NET的快速信息化系统开发框架 — 系列目录

RDIFramework.NET ━ .NET快速信息化系统开发框架 ━ 工作流程组件介绍

RDIFramework.NET框架SOA解决方案(集Windows服务、WinForm形式与IIS形式发布)-分布式应用

RDIFramework.NET代码生成器全新V3.5版本发布-重大升级


一路走来数个年头,感谢RDIFramework.NET框架的支持者与使用者,大家可以通过下面的地址了解详情。

RDIFramework.NET官方网站:www.rdiframework.net/

RDIFramework.NET官方博客:blog.rdiframework.net/

同时需要说明的,以后的所有技术文章以官方网站为准,欢迎大家收藏!

RDIFramework.NET框架由专业团队长期打造、一直在更新、一直在升级,请放心使用!

欢迎关注RDIFramework.net框架官方公众微信(微信号:guosisoft),及时了解最新动态。

扫描二维码立即关注

本文转载自: 掘金

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

Nginx OpenResty Tengine--到底该爱谁

发表于 2021-01-16

大名鼎鼎的Nginx,以高性能著称,号称能够抗下单机数万并发,想必也不用过多介绍了。

在Nginx基础上,还有两款产品:Tengine & OpenResy:

Tengine:

由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性。

OpenResty:

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关,从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。

既然都是基于Nginx,同等条件下在性能方面自然是差异不大的,那么,Tengine和OpenResty跟Nginx的差别到底在哪里呢!?

首先从模块方面做个对比

绿色为Nginx共有模块,橙色为Tengine模块,紫色为OpenResty模块

从表格中显而易见,OpenResty的模块要比前两者多得多,虽然它基于 Nginx 实现,但其适用范围,早已远远超出反向代理和负载均衡。

强在哪里?

  • 它的核心是基于 Nginx 的一个 C 模块(lua-nginx-module),该模块将 LuaJIT 嵌入到 Nginx 服务器中,并对外提供一套完整的 Lua API。
  • 我们可以用 Lua 语言来进行字符串和数值运算、查询数据库、发送 HTTP 请求、执行定时任务、调用外部命令等,还可以用 FFI 的方式调用外部 C 函数。这基本上可以满足服务端开发需要的所有功能。
  • 很多的OpenResty 使用者,都把 OpenResty 用在 API 网关的开发上。如Orange、Kong、APISIX,他们都是实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。
  • github.com/openresty/o…
  • github.com/apache/apis…

更新频率

Nginx–持续更新

OpenResty–紧跟Nginx步伐

Tengine–这是不再维护的节奏呀

最新版本的Tengine-2.3.2基于Nginx-1.17.3。

OpenResty更新迭代非常快,跟nginx也非常紧,社区庞大,一大堆优秀的人在推,支持新的内容也快,使用起来比较放心。

所以,个人最推荐OpenResty,
不知各位会把手中的选票投给谁呢!

欢迎大家关注我的公众号,我是鑫哥,一枚程序猿!

本文转载自: 掘金

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

1…735736737…956

开发者博客

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