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

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


  • 首页

  • 归档

  • 搜索

Flink 从0-1实现 电商实时数仓 - DWD(中)

发表于 2021-08-27

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

5. 定义数据库配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码public class TmallConfig {

/**
* hbase 数据库
*/
public static final String HBASE_SCHEMA = "TMALL_REALTIME";

/**
* phoenix 连接地址
*/
public static final String PHOENIX_SERVER = "jdbc:phoenix:hd1,hd2,hd3:2181";

/**
* clickhouse 连接地址
*/
public static final String CLICKHOUSE_URL="jdbc:clickhouse://hd1:8123/tmall_realtime";

}
6. 定义CDC操作类型枚举类
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
arduino复制代码public enum CDCTypeEnum {

/**
* CDC 中的 c 操作类型 转成 INSERT
*/
INSERT("c"),
/**
* CDC 中的 u 操作类型 转成 UPDATE
*/
UPDATE("u"),
/**
* CDC 中的 d 操作类型 转成 DELETE
*/
DELETE("d");

private static final Map<String, CDCTypeEnum> MAP = new HashMap<>();

static {
for (CDCTypeEnum cdcTypeEnum : CDCTypeEnum.values()) {
MAP.put(cdcTypeEnum.op, cdcTypeEnum);
}
}

String op;

CDCTypeEnum(String op) {
this.op = op;
}

public static CDCTypeEnum of(String c) {
return MAP.get(c);
}
}
7. 封装 Kafka 工具类
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
typescript复制代码public class KafkaUtil {

private static final String KAFKA_SERVER = "hd1:9092,hd2:9092,hd3:9092";

/**
* 获取 kafka 通用配置
* @return 配置类
*/
private static Properties getProps() {
Properties properties = new Properties();
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_SERVER);
return properties;
}

/**
* 通过 topic 和 groupId 创建一个 Kafka Source
* @param topic
* @param groupId
* @return Kafka Source
*/
public static SourceFunction<String> ofSource(String topic, String groupId) {
Properties props = getProps();
props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), props);
}

/**
* 通过 topic 创建一个字符串序列化的 Kafka Sink
* @param topic
* @return Kafka Sink
*/
public static SinkFunction<String> ofSink(String topic) {
return new FlinkKafkaProducer<>(topic, new SimpleStringSchema(), getProps());
}

/**
* 通过 序列化器 创建一个 Kafka Sink
* @param serializationSchema
* @param <IN>
* @return Kafka Sink
*/
public static <IN> SinkFunction<IN> ofSink(KafkaSerializationSchema<IN> serializationSchema) {
return new FlinkKafkaProducer<>("default_topic", serializationSchema, getProps(), FlinkKafkaProducer.Semantic.EXACTLY_ONCE);
}

/**
* 通过 topic 和 groupId 生成一个 Flink SQL 的 Kafka 连接信息
* @param topic
* @param groupId
* @return
*/
public static String getKafkaDDL(String topic, String groupId) {
return " 'connector' = 'kafka', " +
" 'topic' = '" + topic + "'," +
" 'properties.bootstrap.servers' = '" + KAFKA_SERVER + "', " +
" 'properties.group.id' = '" + groupId + "', " +
" 'format' = 'json', " +
" 'scan.startup.mode' = 'latest-offset' ";
}
}
8. 封装 redis 工具类,缓存DIM数据
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
csharp复制代码public class RedisUtil {

private static volatile JedisPool jedisPool;

public static Jedis getJedis() {
if (jedisPool == null) {
synchronized (RedisUtil.class) {
if (jedisPool == null) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大可用连接数
poolConfig.setMaxTotal(100);
//连接耗尽是否等待
poolConfig.setBlockWhenExhausted(true);
//等待时间
poolConfig.setMaxWaitMillis(2000);
//最大闲置连接数
poolConfig.setMaxIdle(5);
//最小闲置连接数
poolConfig.setMinIdle(5);
//取连接的时候进行一下测试 ping pong
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig, "hd1", 6379, 1000, "密码");
}
}
}
return jedisPool.getResource();
}

}
9. 封装 Phoenix 工具类,查询 HBase
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
ini复制代码public class PhoenixUtil {

private static Connection connection;

/**
* 查询 一个集合数据
* @param sql sql
* @param clazz 返回集合的类型
* @param <T> 返回集合的类型
* @return 结果集合
*/
public static <T> List<T> queryList(String sql, Class<T> clazz) {
if (connection == null) {
initConnection();
}
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
ResultSet resultSet = preparedStatement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
ArrayList<T> resList = new ArrayList<>();
while (resultSet.next()) {
T t = clazz.newInstance();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
BeanUtils.setProperty(t, metaData.getColumnName(i), resultSet.getObject(i));
}
resList.add(t);
}
resultSet.close();
return resList;
} catch (Exception e) {
e.printStackTrace();
}
return Collections.emptyList();
}

/**
* 初始化 Phoenix 连接
*/
@SneakyThrows
private static void initConnection() {
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
connection = DriverManager.getConnection(TmallConfig.PHOENIX_SERVER);
}

}
10. 封装 DIM 查询 HBase 的工具类
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
typescript复制代码public class DimUtil {

/**
* 删除缓存
* @param tableName
* @param id
*/
public static void delDimCache(String tableName, String id) {
StringBuilder cacheKey = new StringBuilder().append("dim:").append(tableName.toLowerCase()).append(":").append(id);
try (Jedis jedis = RedisUtil.getJedis()) {
jedis.del(cacheKey.toString());
}
}

/**
* 更具 表名 和 id 查询一条数据
* @param tableName 表名
* @param id id
* @return 数据
*/
public static JSONObject getDimInfo(String tableName, String id) {
return getDimInfo(tableName, Tuple2.of("id", id));
}

/**
* 更具 表名 和 多个条件 查询一条数据
* @param tableName 表名
* @param colAndValue 必须的 (条件,值)
* @param colAndValues 可选多个 (条件,值)
* @return 数据
*/
@SafeVarargs
public static JSONObject getDimInfo(String tableName, Tuple2<String, String> colAndValue, Tuple2<String, String>... colAndValues) {
//缓存 key
StringBuilder cacheKey = new StringBuilder().append("dim:").append(tableName.toLowerCase()).append(":").append(colAndValue.f1);
for (Tuple2<String, String> cv : colAndValues) {
cacheKey.append("_").append(cv.f1);
}
try (Jedis jedis = RedisUtil.getJedis()) {
//查缓存
String str = jedis.get(cacheKey.toString());
if (StringUtils.isNotBlank(str)) {
return JSON.parseObject(str);
}
//拼接sql
StringBuilder sql = new StringBuilder();
sql.append("select * from ").append(TmallConfig.HBASE_SCHEMA).append(".").append(tableName)
.append(" where ").append(colAndValue.f0).append("='").append(colAndValue.f1).append("' ");
for (Tuple2<String, String> cv : colAndValues) {
sql.append("and ").append(cv.f0).append("='").append(cv.f1).append("' ");
}
// 查询
List<JSONObject> jsonObjectList = PhoenixUtil.queryList(sql.toString(), JSONObject.class);
if (!jsonObjectList.isEmpty()) {
JSONObject jsonObject = jsonObjectList.get(0);
jedis.setex(cacheKey.toString(), 60 * 60 * 24, jsonObject.toJSONString());
return jsonObject;
}
}
return null;
}

}
11. HBase主意事项
  • 为了开启 hbase 的namespace和phoenix的schema的映射,需要在hbase以及phoenix的hbase-site.xml配置文件中
1
2
3
4
5
6
7
8
9
xml复制代码<property>
<name>phoenix.schema.isNamespaceMappingEnabled</name>
<value>true</value>
</property>

<property>
<name>phoenix.schema.mapSystemTablesToNamespace</name>
<value>true</value>
</property>
  • 也需要将 hbase-site.xml 放到程序中
    image.png

下期预告:DIM层 & DWD层 核心代码

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

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

Mybatis经典面试题总结王者笔记下《收藏版》

发表于 2021-08-27

这是我参与 8 月更文挑战的第 27天,活动详情查看: 8月更文挑战
​

Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种方法是使用标记逐个定义数据库列名和对象属性名之间的映射。

第二种方法是使用SQL列的别名函数将列的别名作为对象属性名写入。

使用列名和属性名之间的映射,Mybatis通过反射创建对象并使用反射对象的属性逐个赋值并返回,如果找不到映射关系,则无法完成赋值。

Mybatis 如何执行批量插入?

  • 首先,创建一个简单的INSERT语句:
1
2
3
sql复制代码<insert id=”insertname”>
insert into names (name) values (#{value})
</insert>

然后在Java代码中执行批量插入操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码list < string > names = new arraylist();
names.add(“fred”);
names.add(“barney”);
names.add(“betty”);
names.add(“wilma”);

// 注意 executortype.batch
sqlsession sqlsession =sqlsessionfactory.opensession(executortype.batch);
try {
namemapper mapper = sqlsession.getmapper(namemapper.class);
for (string name: names) {
mapper.insertname(name);
}
sqlsession.commit();
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
throw e;
} finally {
sqlsession.close();
}

Mybatis 如何获取自动生成的(主)键值?

Mapper文件insert语句设置

1
ini复制代码useGeneratedKeys="true" keyProperty="id"

Mybatis 在 mapper 中如何传递多个参数?

第一种方案 DAO层的函数方法

1
arduino复制代码Public User selectUser(String name,String area);

对应的Mapper.xml配置文件

1
2
3
csharp复制代码<select id="selectUser" resultMap="BaseResultMap" parameterType="java.lang.String">  
select * from user_user_t where user_name = #{0} and user_area=#{1}
</select>

其中,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。

第二种Dao层的函数方法

1
less复制代码Public User selectUser(@param(“userName”)Stringname,@param(“userArea”)String area);

对应的Mapper.xml配置文件

1
2
3
sql复制代码<select id=" selectUser" resultMap="BaseResultMap">  
select * from user_user_t where user_name = #{userName,jdbcType=VARCHAR} and user_area=#{userArea,jdbcType=VARCHAR}
</select>

个人觉得这种方法比较好,能让开发者看到dao层方法就知道该传什么样的参数,比较直观,个人推荐用此种方案。

Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,

执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。

动态 sql有九种、具体是:trim | where | set | foreach | if | choose| when | otherwise | bind。

具体九种动态SQL举例:

if标签

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!-- 查询学生list,like姓名 -->  

<select id=" getStudentListLikeName " parameterType="StudentEntity" resultMap="studentResultMap">  

    SELECT * from STUDENT_TBL ST   

    <if test="studentName!=null and studentName!='' ">  

        WHERE ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%')   

    </if>  

</select>

where标签

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码
<!-- 查询学生list,like姓名,=性别 -->
<select id="getStudentListWhere" parameterType="StudentEntity" resultMap="studentResultMap">
SELECT * from STUDENT_TBL ST
<where>
<if test="studentName!=null and studentName!='' ">
ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%')
</if>
<if test="studentSex!= null and studentSex!= '' ">
AND ST.STUDENT_SEX = #{studentSex}
</if>
</where>
</select>

set标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<!-- 更新学生信息 -->  
<update id="updateStudent" parameterType="StudentEntity">
UPDATE STUDENT_TBL
<set>
<if test="studentName!=null and studentName!='' ">
STUDENT_TBL.STUDENT_NAME = #{studentName},
</if>
<if test="studentSex!=null and studentSex!='' ">
STUDENT_TBL.STUDENT_SEX = #{studentSex},
</if>
<if test="studentBirthday!=null ">
STUDENT_TBL.STUDENT_BIRTHDAY = #{studentBirthday},
</if>
<if test="classEntity!=null and classEntity.classID!=null and classEntity.classID!='' ">
STUDENT_TBL.CLASS_ID = #{classEntity.classID}
</if>
</set>
WHERE STUDENT_TBL.STUDENT_ID = #{studentID};
</update>

trim标签

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码 <!-- 查询学生list,like姓名,=性别 -->  
<select id="getStudentListWhere" parameterType="StudentEntity" resultMap="studentResultMap">
SELECT * from STUDENT_TBL ST
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="studentName!=null and studentName!='' ">
ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%')
</if>
<if test="studentSex!= null and studentSex!= '' ">
AND ST.STUDENT_SEX = #{studentSex}
</if>
</trim>
</select>

choose when otherwise标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<!-- 查询学生list,like姓名、或=性别、或=生日、或=班级,使用choose -->  
<select id="getStudentListChooseEntity" parameterType="StudentEntity" resultMap="studentResultMap">
SELECT * from STUDENT_TBL ST
<where>
<choose>
<when test="studentName!=null and studentName!='' ">
ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%')
</when>
<when test="studentSex!= null and studentSex!= '' ">
AND ST.STUDENT_SEX = #{studentSex}
</when>
<when test="studentBirthday!=null">
AND ST.STUDENT_BIRTHDAY = #{studentBirthday}
</when>
<when test="classEntity!=null and classEntity.classID !=null and classEntity.classID!='' ">
AND ST.CLASS_ID = #{classEntity.classID}
</when>
<otherwise>

</otherwise>
</choose>
</where>
</select>

foreach

1
2
3
4
5
6
7
sql复制代码<select id="getStudentListByClassIDs" resultMap="studentResultMap">  
SELECT * FROM STUDENT_TBL ST
WHERE ST.CLASS_ID IN
<foreach collection="list" item="classList" open="(" separator="," close=")">
#{classList}
</foreach>
</select>

Mybatis 的 Xml 映射 文件 中,不同 的 Xml 映射 文件 , id 是否 可以 重复 ?

不同的Xml映射文件 ,如果配置了namespace,那么id可以重复;

如果没有配置namespace,那么id不能重复;原因就是namespace+id是作为Map<String, MapperStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。

有了namespace,自然id就可以重复 ,namespace不同 ,namespace+id自然也就不同 。

Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?

、、、、 ,加上动态 sql 的 9 个标签,其中 为 sql 片段标签,通过 标签引入 sql 片段, 为不支持自增的主键生成策略标签。

为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate属于全自动ORM映射工具 ,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取 ,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以 ,称之为半自动ORM映射工具。

Mybatis 的一对一、一对多的关联查询 ?

一对一关联查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<mapper namespace="com.lcb.mapping.userMapper">  
<!--association 一对一关联查询 -->
<select id="getClass" parameterType="int" resultMap="ClassesResultMap">
select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}
</select>

<resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
<!-- 实体类的字段名和数据表的字段名映射 -->
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
</resultMap>

</mapper>

一对多关联查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<!--collection  一对多关联查询 -->  
<select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
select * from class c,teacher t,student s where c.teacher_id=t.t_id and c.c_id=s.class_id and c.c_id=#{id}
</select>

<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>

<collection property="student" ofType="com.lcb.user.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
</resultMap>

MyBatis 实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询,

联合查询是几个表联合查询,只查询一次,通过在resultMap里面配置association节点配置一对一的类就可以完成;

嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置

MyBatis 实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。

联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。

Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?

**Mybatis仅支持****association****关联对象和****collection****关联集合对象的延迟加载** **,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。**

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

Mybatis 的一级、二级缓存

一级缓存:

**Mybatis支持缓存,但在没有配置的情况下,默认情况下它只启用一级缓存。级别1缓存只对相同的SqlSession启用。因此,如果SQL参数一模一样,我们使用相同的SqlSession对象调用映射方法,通常只执行SQL一次,因为第一个查询使用SelSession MyBatis将把它放在缓存中,和将来查询,如果没有声明需要刷新,如果缓存中没有,SqlSession将获取当前缓存的数据,并且不会再次向数据库发送SQL**

)​

二级缓存:

  MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。二级缓存与一级缓存其机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(Namespace),并且可自定义存储源,如Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置;

)​

对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了C/U/D操作后,默认该作用域下所有select中的缓存将被clear。

什么是 MyBatis 的接口绑定?有哪些实现方式?

**接口绑定** **,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。**

接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上@Select、@Update等注解,里面包含Sql语句来绑定;

另外一种就是通过xml里面写SQL来绑定,在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。

当Sql语句比较简单时候,用注解绑定,当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。

使用 MyBatis 的 mapper 接口调用时有哪些要求?

Mapper接口方法名和mapper.xml中定义的每个sql的id相同;

Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同;

Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;

Mapper.xml文件中的namespace即是mapper接口的类路径。

Mapper 编写的几种实现方式?

第一、接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。

1、在 sqlMapConfig.xml 中配置 mapper.xml 的位置

1
2
3
4
5
6
7
8
ini复制代码
<mappers>

<mapper resource="mapper.xml 文件的地址" />

<mapper resource="mapper.xml 文件的地址" />

</mappers>

2、定义 mapper 接口.

3、实现类集成 SqlSessionDaoSupportmapper 方法中可以this.getSqlSession()进行数据增删改查。

4、spring 配置

1
2
3
4
5
6
ini复制代码
<bean id=" " class="mapper 接口的实现">

 <property name="sqlSessionFactory" ref="sqlSessionFactory"></property>

</bean>

第二、使用 org.mybatis.spring.mapper.MapperFactoryBean:

1、在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置

1
2
3
4
5
6
7
ini复制代码<mappers>

<mapper resource="mapper.xml 文件的地址" />

<mapper resource="mapper.xml 文件的地址" />

</mappers>

2、定义 mapper 接口:

2.1、mapper.xml 中的 namespace 为 mapper 接口的地址

2.2、mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致

2.3、Spring 中定义

1
2
3
4
5
6
7
8
ini复制代码
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">

<property name="mapperInterface" value="mapper 接口地址" />

<property name="sqlSessionFactory" ref="sqlSessionFactory" />

</bean>

第三、使用 mapper 扫描器:

1、mapper.xml 文件编写:mapper.xml 中的 namespace 为 mapper 接口的地址;mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。

2、定义 mapper 接口:注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录

3、配置 mapper 扫描器:

1
2
3
4
5
6
7
ini复制代码<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="mapper 接口包地址"></property>

<property name="sqlSessionFactoryBeanName"value="sqlSessionFactory"/>

</bean>

4、使用扫描器后从 spring 容器中获取 mapper 的实现对象

简述 Mybatis 的插件运行原理,以及如何编写一个插件?

**Mybatis只能为ParameterHandler, ResultSetHandler,StatementHandler和Executor接口。Mybatis使用JDK的动态生成为需要被拦截的接口生成代理对象,以便在执行这4种类型时实现接口方法拦截Invoke (), Invoke (), Invoke ()、当然,方法只会拦截那些您指定需要拦截的方法。**

编写插件:实现Mybatis的Interceptor接口

1
2
3
4
5
6
7
8
9
10
typescript复制代码public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}

复制intercept()方法插件会编写注解来指定要拦截接口的哪些方法。记住不要忘记配置文本配置你编写的插件哈。

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
typescript复制代码public class Invocation {

private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}

public Object getTarget() {
return target;
}

public Method getMethod() {
return method;
}

public Object[] getArgs() {
return args;
}

public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}

}

方法说明:这个东西包含了四个概念:

  • target 拦截的对象
  • method 拦截target中的具体方法,也就是说Mybatis插件的粒度是精确到方法级别的。
  • args 拦截到的参数。
  • proceed 执行被拦截到的方法,你可以在执行的前后做一些事情。

总结:

好了,今天就分享到这里啦、这篇文章总体来说对于学习或面试来说都是比较不错的、文章中涉及的知识点都比较关键。

​

本文转载自: 掘金

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

太牛逼了,Markdown 几行字符就可以生成思维导图了!

发表于 2021-08-27

这是我参与 8 月更文挑战的第 27 天,活动详情查看: 8月更文挑战

一名致力于在技术道路上的终身学习者、实践者、分享者,一位忙起来又偶尔偷懒的原创博主,一个偶尔无聊又偶尔幽默的少年。

欢迎各位掘友们微信搜索「杰哥的IT之旅」关注!

原文链接:太牛逼了,Markdown 几行字符就可以生成思维导图了!

大家好,我是 JackTian。

我第一次接触Markdown 编辑器,还是从运营这个公众号的一段日子里说起。

Markdown

刚开始的文章排版很乱,Markdown 编辑器这东西也从来没听说过,随着时间的流逝,认识了业界内的大佬,从中吸取了经验,后来我才开始逐步抛弃富文本编辑器,拥抱 Markdown 编辑器。

首先,来说说我是为什么拥抱Markdown编辑器的?经常写作的朋友知道,之前使用富文本编辑器时,写好的文章会经过反复的调整样式、修饰等一系列的操作,虽然看上去没什么难度,但当一篇文章反反复复调整时,就觉得很繁琐,所以在文章排版中要花费很长时间。

直到后来遇到了Markdown 编辑器,用了一段时间后,发现Markdown 编辑器的最基本语法就可以满足我日常的写作与排版。但是,Markdown 编辑器的劣势是易见的,自身支持的文本结构是有限的。

Markdown 编辑器没有对标签进行样式渲染的能力,因此需要依赖第三方的解析和渲染,所以各个 Markdown 编辑器对 Markdown 语法的样式渲染都不一样。

而对于一些写作者来说,对文章排版没有过多的花里胡哨设计,Markdown 的基本语法以及对文本结构也和第三方之间存在差异化,也可以根据自身所需选择适合自己的编辑器,其语法和文本结构都是统一的,因此你也不用担心在编写过程中的排版和设计与其最终所展示出的结果,所以这也是我后来逐步拥抱 Markdown 的原因之一。

思维导图

思维导图,给我的第一感觉就是将自己脑海里面的某一种东西进行详细的拆分,与其将核心点、关键点进行罗列,使得给他人查看时,很清晰明了,大致通过一张思维导图就可以知道你的这篇文章分为哪些部分等等,思维导图也就成为了日常提高效率的一款工具了。

之前我在写 iPtables 文章时,就采用了思维导图的方式,将一篇文章的结构框架、子框架,然后在具体划分到内容知识点等,比如结构分为:文章标题、子标题、子标题内容、副标题、副标题内容等;

image.png

对于思维导图,我之前一直用的两个工具,推荐给大家。

  • 幕布:mubu.com/
  • processon:www.processon.com/

总之,不管你使用哪种方式绘制思维导图,其最终目的都是为了能够高效的呈现出来,通常情况下,编辑完的思维导图都是导出后保存下来,再分享给他人。但有时候,因为导图绘制内容层级过多,占用大量的屏幕,甚至连放大后的一些字体都看得模糊不清,给人的效果很不好。

Markmap

以 Markdown 绘制思维导图的开源工具——markmap

image.png

markmap-lib 作为渲染 Markdown 思维导图的可视化组件。

image.png

如果你会用 Markdown 基本语法,那么这款 Markmap 思维导图工具也会很快上手的,如果不会 Markdown 的语法。

即便你第一次听说 Markdown 也没关系,也不会影响你用 Markmap 的烦恼,哪怕你之前使用的其他什么思维导图工具,都是通过频繁的设计、选择图形、还是画线等等,通过 Markmap 即可一切帮你搞定。

Markmap 支持直接将 Markdown 语法内容按照标题到内容的顺序渲染为 SVG 格式的思维导图,渲染出来的导图可缩放、也可展开收起子节点,样式也非常美观。

image.png

使用 Markmap 绘制思维导图,只需三个符号。

  • #:标题
  • -:列表
  • —:分隔符

通过使用如上三个符号,即可轻松的添加子分支。同时为了能够更好的与 Markdown 配合,通过使用 Markdown 的代码块和自定义的语法来作为在 Markdown 中绘制思维导图的语法,如下所示:

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
markdown复制代码# My map

## Search sites

- [Google](https://www.google.com/)
- [Baidu](https://www.baidu.com/)
- [Bing](https://www.bing.com/)
- [pixabay](https://pixabay.com/)

## WeChat public number

### The preparatory work

- The theme
- conceived
- writing
- typography
- Adjustment and supplement
- Design cover drawing
- Regularly send

### Late work

- Public number text reading
- The reader forward
- The secondary transfer
- Circle of friends
- community
- ......

---

- learning
- The input
- The output

由此,我们在通过 Markdown 编辑器编写文章时,就可以通过添加块代码的方式来快速绘制思维导图,如上代码实现后的结果所示:

image.png

Markmap 优点

  • 轻量化、在线直接使用;
  • 开源免费;
  • 支持超链接;
  • 语法字符简单,一键生成导图;
  • 可下载为 SVG 和 HTML 格式的文件;

扩展地址

  • markmap:
    地址:markmap.js.org/repl/
  • GitHub markmap:
    地址:github.com/dundalek/ma…
  • GitHub markmap-lib:
    地址:github.com/gera2ld/mar…

总结

Markmap 这款思维导图工具可有效的帮助经常编写文档的小伙伴们,在编写完后,生成一个类似于文档目录的思维导图,简单明了的可以看出该文章包含的哪些章节,如果你有经常绘制思维导图的习惯,不妨可以试试 Markmap。

推荐阅读

为什么要学习 Markdown?究竟有什么用?

推荐几款基于 Markdown 在线制作简历的网站

本文完。


原创不易,如果你觉得这篇文章对你有点用的话,麻烦你为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

对了,掘友们记得给我点个免费的关注哟!防止你迷路下次就找不到我了。

我们下期再见!

本文转载自: 掘金

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

DockerFile详解&实战

发表于 2021-08-26

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

之前文章中我们初步了解了dockerfile,知道dockerfile是用来构建docker镜像的文件,也就是命令参数脚本,现在我们尝试深入学习下dockerfile

先了解下构建步骤dockerfile构建的完整步骤:

  • 编写一个dockerfile文件
  • docker build 构建成为一个镜像
  • docker run 运行镜像
  • docker push 发布镜像(DokcerHub、阿里云镜像仓库)

看下官方是怎么做的

1、在dockerhub上随便搜索一个镜像

2、随便选择一个版本会直接跳转到github上,并且可以看到镜像的命令脚本

很多官方的镜像是一个很纯净的包,我们可以按照自己的需求制作自己的镜像

Dockerfile的构建

dockerfile的构建并不难,主要我们要把命令脚本写好

基础知识

1、每个关键字(指令)都必须是大写字母

2、执行是从上到下顺序执行的

3、#表示注释

4、每一个指令都会创建提交一个新的镜像层并提交

dockerfile是面向开发的,我们后面发布项目,做镜像,就需要编写dockerfile这个文件(dockerfile逐渐成为企业交付的标准)

我们在回顾下几个概念

  • dockerfile:构建文件,定义所有步骤,源代码
  • dockerImages:通过dockerfile构建生成镜像,最终发布和运行的产品
  • docker容器:容器就是镜像运行起来提供服务器

DockerFile的指令

1
2
3
4
5
6
7
8
9
10
11
shell复制代码FROM 			# 基础镜像,一切从这里开始
MAINTAINER # 镜像是谁写的
RUN # 镜像构建是需要运行的命令
ADD # 步骤
WORKDIR # 镜像的工作目录
VOLUME # 挂载的目录
EXPOSE # 保留端口配置
CMD # 指定这个容器启动时要运行的命令,只有最后一个会生效
ENTRYPOINT # 指定这个容器启动时要运行的命令,可以追加命令
COPY # 类似ADD,将文件拷贝到镜像中
ENV # 构建时候设置的环境变量

实战测试

从上面centos命令脚本中可以看到开头FROM scratch,dockerhub中99%的镜像都是从FROM scratch这个基础镜像来的,然后配置需要的软件和配置来构建的。

在官方的centos中对于一些基础的命令都是没有的,所以尝试创建一个自己的centos试一下

1
2
3
4
5
6
ruby复制代码$ docker run -it centos
[root@8fbe85e4a93b /]# vim
bash: vim: command not found
[root@8fbe85e4a93b /]# ifconfig
bash: ifconfig: command not found
[root@8fbe85e4a93b /]#

第一步:编写文件

在指定路径下编写dockerfile的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码FROM centos

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN yum -y install vim
RUN yum -y install net-tools

EXPOSE 80

CMD echo $MYPATH
CMD echo "---end---"
CMD /bin/bash

第二步:构建镜像

mydockerfile是文件名

1
复制代码docker build -f mydockerfile -t mycentos:0.1 .

可以看到是按照我们编写的指令逐步构建的

img

查看下生成的镜像

img

第三步:测试运行

1
arduino复制代码docker run -it mycentos:0.1

运行进入容器后,可以看到进入的是配置的工作目录,并且ifconfig命令也是可以使用的,尝试用vim命令也是ok的,所以我们自己通过dockerfile写的镜像是可以正常使用的

img

我们可以列出本地镜像的变更历史,通过历史可以看下这个镜像是怎么做起来的。这样的话我们可以研究下平时使用的镜像是怎么做的

1
bash复制代码docker history

img

实战tomcat镜像

准备镜像文件

准备镜像文件tomcat压缩包,jdk压缩包

编写dockerfile

编写dockerfile文件,官方命名Dockerfile,build的时候可以自动寻找这个文件,不需要-f指定了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码FROM centos
MAINTAINER ajajaj

COPY readme.txt /usr/local/readme.txt

ADD jdk-8u301-linux-x64.tar /usr/local/
ADD apache-tomcat-8.5.70.tar.gz /usr/local/

RUN yum -y install vim

ENV MYPATH /usr/local
WORKDIR $MYPATH

ENV JAVA_HOME /usr/local/jdk1.8.0_301
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-8.5.70
ENV CATALINA_BASH /usr/local/apache-tomcat-8.5.70
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin

EXPOSE 8080

CMD /usr/local/apache-tomcat-8.5.70/bin/startup.sh && tail -F
/usr/local/apache-tomcat-8.5.70/bin/logs/catalina.out

构建镜像

编写好了dockerfile可以构建了

1
erlang复制代码docker build -t diytomcat .

到这来自定义的tomcat镜像就创建成功了

启动镜像

然后我们启动这个镜像,并且顺便做了些操作,可以仔细看下下面的命令

  • 端口映射到9090
  • 工程目录、日志目录进行挂载
1
shell复制代码docker run -d -p 9090:8080 --name ajtomcat -v /Users/cb/test/tomcat-out/test:/usr/local/apache-tomcat-8.5.70/webapps/test -v /Users/cb/test/tomcat-out/tomcatlogs:/usr/local/apache-tomcat-8.5.70/logs diytomcat

进入容器

1
2
shell复制代码$ docker exec -it 9f3f7ed2b184 /bin/bash
[root@9f3f7ed2b184 local]#

访问测试

访问下tomcat(curl localhost:8080),是通的哦

宿主机访问下9090看下也是OK的哦

发布项目

由于做了卷挂载,我们之间在本地编写项目就可以了

上面的命令的挂载关系

/Users/cb/test/tomcat-out/test:/usr/local/apache-tomcat-8.5.70/webapps/test

我们在外部路径下创建一个最简单的web工程

  • 创建一个WEB-INF文件夹,文件夹中新建一个web.xml文件
  • 创建一个index.jsp文件

文件中按照格式随便写点东西

web.xml

1
2
3
4
5
6
7
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
</web-app>

index.jsp

1
2
3
4
5
6
7
8
9
jsp复制代码<html>
<head><title>Hello World</title></head>
<body>
Hello World!<br/>
<%
out.println("Your IP address is " + request.getRemoteAddr());
%>
</body>
</html>

搞定之后我们来访问下这个web工程

最后我们来看下日志的输出情况,由下图可见日志也是稳稳的打印了

OK,到这里我们就算成功了

现在我们在本地就可以完成项目的实时发布,而且项目是部署在docker中的,最重要的是docker镜像使我们纯手工打造的哦,这一套完成的流程我们就算搞定了…

docker学习到这里算是对docker有了一个比较全面的认识了,后面还会继续深入的再研究下docker网络、镜像发布等一些东西,希望这个文章对大家有一点帮助!

有时我们的眼睛可以看见宇宙,却看不见社会底层最悲惨的世界 –《十宗罪》

本文转载自: 掘金

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

Flink 从0-1实现 电商实时数仓 - ODS & DW

发表于 2021-08-26

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

ODS 层

采集到 kafka 直接作为 ODS 层,不需要额外处理,保持数据原貌。

日志数据主题:ods_base_log

业务数据主题:ods_base_db_m

DWD 层

日志 DWD 层

  我们前面采集的日志数据已经保存到Kafka中,作为日志数据的ODS层,从kafka的ODS层读取的日志数据分为3类, 页面日志、启动日志和曝光日志。这三类数据虽然都是用户行为数据,但是有着完全不一样的数据结构,所以要拆分处理。将拆分后的不同的日志写回Kafka不同主题中,作为日志DWD层。

  页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光侧输出流。

1. 分析主要任务

  • 接收Kafka数据,过滤空值数据

对Maxwell抓取的数据进行ETL,保留有用的部分,过滤掉没用的

  • 实现 动态 分流功能

 由于MaxWell是把全部数据统一写入一个Topic中, 这样显然不利于日后的数据处理。所以需要把各个表拆开处理。但是由于每个表有不同的特点,有些表是维度表,有些表是事实表,有的表既是事实表在某种情况下也是维度表。

 在实时计算中一般把维度数据写入存储容器,一般是方便通过主键查询的数据库比如HBase,Redis,MySQL等。一般把事实数据写入流中,进行进一步处理,最终形成宽表。但是作为Flink实时计算任务,如何得知哪些表是维度表,哪些是事实表呢?而这些表又应该采集哪些字段呢?

 我们可以将上面的内容放到某一个地方,集中配置。这样的配置不适合写在配置文件中,因为业务端随着需求变化每增加一张表,就要修改配置重启计算程序。所以这里需要一种动态配置方案,把这种配置长期保存起来,一旦配置有变化,实时计算可以自动感知。

这种可以有两个方案实现

+ 一种是用Zookeeper存储,通过Watch感知数据变化。
+ 另一种是用mysql数据库存储,周期性的同步,使用 `FlinkCDC` 读取。这里选择第二种方案,主要是mysql对于配置数据初始化和维护管理,用sql都比较方便。

如图:

 业务数据保存到 Kafka 的主题中

 维度数据保存到 Hbase 的表中
image.png

2. 代码实现

1. 建 配置表
1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `table_process` (
`source_table` varchar(200) NOT NULL COMMENT '来源表',
`operate_type` varchar(200) NOT NULL COMMENT '操作类型 insert,update,delete(用于过滤是否处理 某些操作的数据)',
`sink_type` varchar(200) DEFAULT NULL COMMENT '输出类型 hbase kafka',
`sink_table` varchar(200) DEFAULT NULL COMMENT '输出表(主题)',
`sink_columns` varchar(2000) DEFAULT NULL COMMENT '输出字段(用于过滤一些不需要的字段)',
`sink_pk` varchar(200) DEFAULT NULL COMMENT '主键字段',
`sink_extend` varchar(200) DEFAULT NULL COMMENT '建表扩展(建hbase表时,的表配置)',
PRIMARY KEY (`source_table`,`operate_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2. 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
xml复制代码<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba.ververica</groupId>
<artifactId>flink-connector-mysql-cdc</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-spark</artifactId>
<version>5.0.0-HBase-2.0</version>
<exclusions>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</exclusion>
</exclusions>
</dependency>
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
java复制代码@Data
public class TableProcess {

/**
* 动态分流Sink常量 改为小写和脚本一致
*/
public static final String SINK_TYPE_HBASE = "hbase";
public static final String SINK_TYPE_KAFKA = "kafka";
public static final String SINK_TYPE_CK = "clickhouse";

/**
* 来源表
*/
private String sourceTable;

/**
* 操作类型 insert,update,delete
*/
private String operateType;

/**
* 输出类型 hbase kafka
*/
private String sinkType;

/**
* 输出表(主题)
*/
private String sinkTable;

/**
* 输出字段
*/
private String sinkColumns;

/**
* 主键字段
*/
private String sinkPk;

/**
* 建表扩展
*/
private String sinkExtend;
}
4. 在MySQL Binlog 添加对配置数据库的监听,并重启MySQL

修改配置文件

1
bash复制代码sudo vim /etc/my.cnf

把存放配置数据库(tmall_realtime)添加至 binlog-do-db

1
2
3
4
5
ini复制代码server-id=1
log-bin=mysql-bin
binlog_format=row
binlog-do-db=tmall
binlog-do-db=tmall_realtime

下期预告:DWD层 用到的工具类

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

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

Python实现发送邮件(实现单发/群发邮件验证码) smt

发表于 2021-08-26

这是我参与8月更文挑战的第26天,活动详情查看: 8月更文挑战

Python smtplib 教程展示了如何使用 smtplib 模块在 Python 中发送电子邮件。 要发送电子邮件,我们使用 Python 开发服务器,Mailtrap 在线服务和共享的网络托管邮件服务器。

smtplib库

python发送邮件需要用到smtplib库,先简单了解一下

SMTP

简单邮件传输协议(SMTP)是用于电子邮件传输的通信协议。 Is 是一个 Internet 标准,该标准于 1982 年由 RFC 821 首次定义,并于 2008 年由 RFC 5321 更新为扩展 SMTP 添加。 邮件服务器和其他邮件传输代理使用 SMTP 发送和接收邮件。

smtplib是一个 Python 库,用于使用简单邮件传输协议(SMTP)发送电子邮件。 smtplib是内置模块; 我们不需要安装它。 它抽象了 SMTP 的所有复杂性。

邮件服务器

要实际发送电子邮件,我们需要有权访问邮件服务器。 Python 带有一个简单的开发邮件服务器。 Mailslurper 是易于使用的本地开发服务器。 共享的虚拟主机提供商使我们可以访问邮件服务器。 我们可以在帐户中找到详细信息。

smtp协议的基本命令包括:\

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码    HELO 向服务器标识用户身份\
    MAIL 初始化邮件传输 mail from:\
    RCPT 标识单个的邮件接收人;常在MAIL命令后面,可有多个rcpt to:\
    DATA 在单个或多个RCPT命令后,表示所有的邮件接收人已标识,并初始化数据传输,以.结束\
    VRFY 用于验证指定的用户/邮箱是否存在;由于安全方面的原因,服务器常禁止此命令\
    EXPN 验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用\
    HELP 查询服务器支持什么命令\
    NOOP 无操作,服务器应响应OK\
    QUIT 结束会话\
    RSET 重置会话,当前传输被取消\
    MAIL FROM 指定发送者地址\
    RCPT TO 指明的接收者地址

实战

1.126邮箱一般默认关闭SMTP服务,我们得先去开启它

2.Python代码如下

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
python复制代码
# smtplib 用于邮件的发信动作
import smtplib
from email.mime.text import MIMEText
# email 用于构建邮件内容
from email.header import Header
# 用于构建邮件头
# 发信方的信息:发信邮箱,126 邮箱授权码
from_addr = 'trobot@126.com'
password = 'POP3/SMTP服务授权密码,上一步可以获取'

# 收信方邮箱
to_addr = 'xxxx@163.com'

# 发信服务器
smtp_server = 'smtp.126.com'


"""标题"""
head="邮箱验证码"
"""正文"""
text="【TRobot】您的验证码32123,该验证码5分钟内有效,请勿泄漏于他人!"


# 邮箱正文内容,第一个参数为内容,第二个参数为格式(plain 为纯文本),第三个参数为编码
msg = MIMEText(text,'plain','utf-8')

# 邮件头信息
msg['From'] = Header(from_addr)
msg['To'] = Header(to_addr)
msg['Subject'] = Header(head)

# 开启发信服务,这里使用的是加密传输
#server = smtplib.SMTP_SSL()
server=smtplib.SMTP_SSL(smtp_server)
server.connect(smtp_server,465)
# 登录发信邮箱
server.login(from_addr, password)
# 发送邮件
server.sendmail(from_addr, to_addr, msg.as_string())
# 关闭服务器
server.quit()

本文转载自: 掘金

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

nvm 安装、卸载与使用(详细步骤)

发表于 2021-08-26

一、node、nvm、npm、npx、nrm 区别

  • node:是一个基于 Chrome V8 引擎的 JS 运行环境。
  • npm:是 node.js 默认的包管理系统(用 JavaScript 编写的),在安装的 node 的时候,npm 也会跟着一起安装,管理 node 中的第三方插件。
  • npx:npm 从 v5.2.0 开始新增了 npx 命令,>= 该版本会自动安装 npx,附带:npx 有什么作用跟意义?为什么要有 npx?什么场景使用?。
  • nrm:是一个 npm 源管理工具,使用它可以快速切换 npm 源,默认是官方源,当 npm 下载包过慢时,可能需要切换到第三方源(例如:淘宝、科大…),还有公司私有源地址等等。
  • nvm:node 版本管理器,也就是说:一个 nvm 可以管理多个 node 版本(包含 npm 与 npx),可以方便快捷的 安装、切换 不同版本的 node。

二、node、nvm、npm、npx、nrm 关系

  • nvm 管理 node (包含 npm 与 npx) 的版本,npm 可以管理 node 的第三方插件,nrm 可以管理 npm 的源地址(当然也可以直接使用 npm 自带命令管理,看个人习惯)。
  • 切换不同的 node 版本,npm 与 npx 的版本也会跟着变化。
1
2
3
4
5
6
7
8
arduino复制代码$ nvm use v8.16.0
Now using node v8.16.0 (npm v6.4.1)

$ nvm use v14.15.4
Now using node v14.15.4 (npm v6.14.10)

$ nvm use v18.6.0
Now using node v18.6.0 (npm v8.13.2)

三、安装 nvm

  • nvm 官方文档,nvm 卸载详细流程,Mac 推荐 HomeBrew 安装,方便更新管理,其他随意能装上就行,附带:Mac Homebrew 安装与卸载。
  • 安装方式一:Mac HomeBrew 安装

如果安装没问题,按照 安装方式三 中除安装部分外,配置好当前解释器需要的 nvm 配置。

1
ruby复制代码$ brew install nvm
  • 安装方式二:命令安装

但是这边网不是很好,一直停在安装页面没动静,所以后面选择了 手动安装。

如果安装没问题,按照 安装方式三 中除安装部分外,配置好当前解释器需要的 nvm 配置。

下面的安装路径,在 nvm 官方文档 中有。

1
2
3
shell复制代码$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
或
$ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

附带:(下面后面找到的替代方案,安装源都换成国内的,尤其是在服务器上进行安装时,网络不行的情况下,可以使用下面这个)

1
2
3
4
5
shell复制代码# 安装
$ bash -c "$(curl -fsSL https://gitee.com/RubyKids/nvm-cn/raw/master/install.sh)"

# 卸载
$ bash -c "$(curl -fsSL https://gitee.com/RubyKids/nvm-cn/raw/master/uninstall.sh)"
  • (推荐)安装方式三:手动安装

1、下载官方 Git nvm

image.png

2、解压文件找到 install.sh

image.png

3、执行命令,将 install.sh 路径放到下面命令中回车运行。

1
shell复制代码$ sh xxx/instal.sh

4、然后等待,安装完成之后。(没有提示则看下一步) 如果提示没有找到对应解释器的配置文件,则需要手动创建一个当前解释器的配置文件,如果不清楚可以看下这篇文章 Shell 切换解释器,查看当前解释器

1
error复制代码=> Profile not found. Tried ~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile.

2022-07-23-12-51-55.png

+ 知道属于什么解释器之后,没有则手动创建一个,这里以 `zsh` 解释器举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码# 创建 zsh 配置文件
$ touch ~/.zshrc

# 编辑配置文件
$ vim ~/.zshrc

```5、如果解释器存在配置文件,则只需要添加到配置文件中即可


+ 将 `nvm` 的配置添加到 `~/.zshrc` 里面保存起来,以安装时提示添加的配置为准,下面就拷贝一下当前安装所提示的。
+ 下面配置二选一即可,但是有一点需要注意:在 `$ sh xxx/instal.sh` 进行安装之前如果本地存在当前解释器的配置文件,默认一般会自动将 `2` 配置直接进行追加到配置文件中,也就是不需要手动进行添加。
+ 在安装这之前没有配置文件的话,就需要先创建配置文件,在手动贴进去,也可以创建配置文件之后,在重新直接运行一遍 `$ sh xxx/instal.sh` 安装,反正也只是覆盖一遍,也会自动追加进去,可以打开配置自己看看。
+ 另外如果没有好的网络,推广更换一下 `nvm` 的镜像地址为淘宝,默认是官方 [nvm 切换为淘宝镜像](https://blog.csdn.net/zz00008888/article/details/132412685) 。
shell复制代码# 1、这是本地不存在配置文件的时候提示需要添加的配置 export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm # 2、这是本地存在配置文件的时候提示需要添加的配置(推荐) export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
1
+ 然后重新加载一遍配置文件,使其生效:
shell复制代码$ source ~/.zshrc
1
2


shell复制代码$ nvm -v
或
$ nvm use system

1
2
3
4
5
6
7
8

![image.png](https://gitee.com/songjianzaina/juejin_p3/raw/master/img/79ec57565b557f47c80344cbc8d30b3cdb6ffbf02b43ff16e5048adc33a9041a)


#### 四、使用 `nvm`


* 安装最新稳定版 `node`,当前是 `node v12.9.1 (npm v7.9.0)`

ruby复制代码$ nvm install stable

1
* 安装指定版本,可模糊安装,如:安装 `v4.4.0`,既可 `$ nvm install v4.4.0`,又可 `$ nvm install 4.4`

ruby复制代码$ nvm install

1
* 删除已安装的指定版本,语法与 `install` 用法一致

ruby复制代码$ nvm uninstall

1
* 切换使用指定的版本 `node`

csharp复制代码// 临时版本 - 只在当前窗口生效指定版本
$ nvm use

// 永久版本 - 所有窗口生效指定版本
$ nvm alias default

1
2
3
4
5
6

`注意`:在任意一个命令行窗口进行切换之后,其他的窗口或其他命令行工具窗口 `需要关掉工具,重启才能生效`。(例如 `VSCode` 内或外部命令切换之后,需要重启 `VSCode`,才能正常生效,否则或处于 `临时生效状态`,也就是在 `VSCode` 中重新打开一个命令行查看版本还会是旧版本,所以必须要重启。)


这里的 `重启` 不是简单的关掉窗口重启,没有退出后台进程,而是完全退出杀死工具进程,重新启动。
* 列出所有安装的版本

shell复制代码$ nvm ls

1
* 列出所有远程服务器的版本(官方 `node version list`)

ruby复制代码$ nvm ls-remote

1
* 显示当前的版本

ruby复制代码$ nvm current

1
* 给不同的版本号添加别名

ruby复制代码$ nvm alias

1
* 删除已定义的别名

shell复制代码$ nvm unalias

1
* 在当前版本 `node` 环境下,重新全局安装指定版本号的 `npm` 包

ruby复制代码$ nvm reinstall-packages

1
* 查看更多命令可在终端输入

ruby复制代码$ nvm



**本文转载自:** [掘金](https://juejin.cn/post/7000652162950758431)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

node+multiparty接受formdata上传文件保

发表于 2021-08-26

前言 之前想做一个接受文件保存到本地的node服务,查遍了全网大部分都是浅尝辄止,点到为止。胡乱贴两段代码完事。经过一段时间的摸索写了一个完整可用的服务,希望能为有相关需要的开发提供一点思路。

效果
在这里插入图片描述

我们前端传输文件一般采用formdata的格式,所以服务端的核心在于怎样去解析formdata格式的数据,目前比较流行的解析插件主要有multiparty与 busboy ,但我看了下大部分的资料都是围绕multiparty的,所以我这边也是采用multiparty进行数据解析。

multiparty官方文档

在引入插件

1
javascript复制代码npm install multiparty

之后,最简单的方法就是直接将需要保存文件的路径传入插件,即可自动解析保存(注意文件路径必须存在不然会报错)

1
2
3
4
5
javascript复制代码(req, res)=>{
var form = new multiparty.Form({uploadDir:'保存文件的地址'})
// 进行解析
form.parse(req)
}

但这种方法插件会将文件先保存到内存然后在从内存保存到硬盘,我们可以直接通过创建流来将数据写入硬盘。

监听part事件

Emitted when a part is encountered in the request. part is a ReadableStream. It also has the following propertie

获取文件可读流,然后根据文件路径创建写入流,然后将文件流写入。

完整代码

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
javascript复制代码const express = require('express');
const multiparty = require('multiparty');
const app = express();
const fs = require('fs');
const path = require('path')
const Busboy = require('busboy');

app.get('/', (req, res) => {
res.send('hello, this is a node server for upload file current env is' + process.env.NODE_ENV)
})
// 跨域
app.use(function (req, res, next) {
if (req.method == 'OPTIONS') res.send('OPTIONS PASS')
res.append('Access-Control-Allow-Origin', '*')
res.append('Access-Control-Allow-Methods', 'OPTIONS, GET, PUT, POST, DELETE')
res.append('Access-Control-Allow-Headers', '*')
next();
});
// 上传
app.post('/upload', (req, res) => {
const form = new multiparty.Form();
try {
form.on('part', async function (part) {
if (part.filename) {
// 获取上传路径
const p = await new Promise((resolve, reject) => {
form.on('field', (name, value) => {
resolve(name == 'path'?value:'')
})
})
const saveTo = (process.env.NODE_ENV == 'dev' ? 'D://Temp' : '/usr/static') + (p || '/default')
// 如果不存在文件夹路径则创建文件夹
await new Promise((resolve, reject) => {
fs.stat(saveTo, (err) => {
if (err) {
fs.mkdirSync(saveTo);
}
resolve()
})
})
// 根据路径创建写入流
const writeStrem = fs.createWriteStream(path.join(saveTo, part.filename))
part.pipe(writeStrem)
}
part.on('error', function (err) {
fileStrem.destroy();
});
});
form.parse(req)
} catch (e) {
console.log(e)
res.send('500')
}
res.send('200')
})
app.listen(8010, function () {
console.log('Example app listening on port 8010!\n');
});

使用busboy同理,也是将文件流进行写入

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
javascript复制代码const express = require('express');
const multiparty = require('multiparty');
const app = express();
const fs = require('fs');
const path = require('path')
const Busboy = require('busboy');

app.get('/', (req, res) => {
res.send('hello, this is a node server for upload file current env is' + process.env.NODE_ENV)
})
// 跨域
app.use(function (req, res, next) {
if (req.method == 'OPTIONS') res.send('OPTIONS PASS')
res.append('Access-Control-Allow-Origin', '*')
res.append('Access-Control-Allow-Methods', 'OPTIONS, GET, PUT, POST, DELETE')
res.append('Access-Control-Allow-Headers', '*')
next();
});
// 上传
app.post('/upload', (req, res) => {
// const form = new multiparty.Form();
// try {
// form.on('part', async function (part) {
// if (part.filename) {
// // 获取上传路径
// const p = await new Promise((resolve, reject) => {
// form.on('field', (name, value) => {
// resolve(name == 'path'?value:'')
// })
// })
// const saveTo = (process.env.NODE_ENV == 'dev' ? 'D://Temp' : '/usr/static') + (p || '/default')
// // 如果不存在文件夹路径则创建文件夹
// await new Promise((resolve, reject) => {
// fs.stat(saveTo, (err) => {
// if (err) {
// fs.mkdirSync(saveTo);
// }
// resolve()
// })
// })
// // 根据路径创建写入流
// const writeStrem = fs.createWriteStream(path.join(saveTo, part.filename))
// part.pipe(writeStrem)
// }
// part.on('error', function (err) {
// fileStrem.destroy();
// });
// });
// form.parse(req)
// } catch (e) {
// console.log(e)
// res.send('500')
// }
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
var saveTo = path.join('D://Temp', path.basename(filename));
file.pipe(fs.createWriteStream(saveTo));
});
busboy.on('finish', function() {
res.writeHead(200, { 'Connection': 'close' });
res.end("That's all folks!");
});
return req.pipe(busboy);
})
app.listen(8010, function () {
console.log('Example app listening on port 8010!\n');
});

本文转载自: 掘金

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

SpringBoot 如何进行参数校验,老鸟们都这么玩的!

发表于 2021-08-26

大家好,我是飘渺。

前几天写了一篇《SpringBoot如何统一后端返回格式?老鸟们都是这样玩的!》阅读效果还不错,而且被很多号主都转载过,今天我们继续第二篇,来聊聊在SprinBoot中如何集成参数校验Validator,以及参数校验的高阶技巧(自定义校验,分组校验)。

此文是依赖于前文的代码基础,已经在项目中加入了全局异常校验器。(代码仓库在文末)

首先我们来看看什么是Validator参数校验器,为什么需要参数校验?

为什么需要参数校验

在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,例如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。靠代码对接口参数一个个校验的话就太繁琐了,代码可读性极差。

Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验,用户名必须位于6到12之间 等等…

Validator校验框架遵循了JSR-303验证规范(参数校验规范), JSR是 Java Specification Requests的缩写。

接下来我们看看在SpringbBoot中如何集成参数校验框架。

SpringBoot中集成参数校验

第一步,引入依赖

1
2
3
4
5
6
7
8
9
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

注:从 springboot-2.3开始,校验包被独立成了一个 starter组件,所以需要引入validation和web,而 springboot-2.3之前的版本只需要引入 web 依赖就可以了。

第二步,定义要参数校验的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Data
public class ValidVO {
private String id;

@Length(min = 6,max = 12,message = "appId长度必须位于6到12之间")
private String appId;

@NotBlank(message = "名字为必填项")
private String name;

@Email(message = "请填写正确的邮箱地址")
private String email;

private String sex;

@NotEmpty(message = "级别不能为空")
private String level;
}

在实际开发中对于需要校验的字段都需要设置对应的业务提示,即message属性。

常见的约束注解如下:

注解 功能
@AssertFalse 可以为null,如果不为null的话必须为false
@AssertTrue 可以为null,如果不为null的话必须为true
@DecimalMax 设置不能超过最大值
@DecimalMin 设置不能超过最小值
@Digits 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
@Future 日期必须在当前日期的未来
@Past 日期必须在当前日期的过去
@Max 最大不得超过此最大值
@Min 最大不得小于此最小值
@NotNull 不能为null,可以是空
@Null 必须为null
@Pattern 必须满足指定的正则表达式
@Size 集合、数组、map等的size()值必须在指定范围内
@Email 必须是email格式
@Length 长度必须在指定范围内
@NotBlank 字符串不能为null,字符串trim()后也不能等于“”
@NotEmpty 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
@Range 值必须在指定范围内
@URL 必须是一个URL

注:此表格只是简单的对注解功能的说明,并没有对每一个注解的属性进行说明;可详见源码。

第三步,定义校验类进行测试

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
java复制代码@RestController
@Slf4j
@Validated
public class ValidController {

@ApiOperation("RequestBody校验")
@PostMapping("/valid/test1")
public String test1(@Validated @RequestBody ValidVO validVO){
log.info("validEntity is {}", validVO);
return "test1 valid success";
}

@ApiOperation("Form校验")
@PostMapping(value = "/valid/test2")
public String test2(@Validated ValidVO validVO){
log.info("validEntity is {}", validVO);
return "test2 valid success";
}

@ApiOperation("单参数校验")
@PostMapping(value = "/valid/test3")
public String test3(@Email String email){
log.info("email is {}", email);
return "email valid success";
}
}

这里我们先定义三个方法test1,test2,test3,test1使用了 @RequestBody注解,用于接受前端发送的json数据,test2模拟表单提交,test3模拟单参数提交。注意,当使用单参数校验时需要在Controller上加上@Validated注解,否则不生效。

第四步,体验效果

  1. 调用test1方法,提示的是org.springframework.web.bind.MethodArgumentNotValidException异常
1
2
3
4
5
6
7
8
9
json复制代码POST http://localhost:8080/valid/test1
Content-Type: application/json

{
"id": 1,
"level": "12",
"email": "47693899",
"appId": "ab1c"
}
1
2
3
4
5
6
json复制代码{
"status": 500,
"message": "Validation failed for argument [0] in public java.lang.String com.jianzh5.blog.valid.ValidController.test1(com.jianzh5.blog.valid.ValidVO) with 3 errors: [Field error in object 'validVO' on field 'email': rejected value [47693899]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@26139123,.*]; default message [不是一个合法的电子邮件地址]]...",
"data": null,
"timestamp": 1628239624332
}
  1. 调用test2方法,提示的是org.springframework.validation.BindException异常
1
2
3
4
json复制代码POST http://localhost:8080/valid/test2
Content-Type: application/x-www-form-urlencoded

id=1&level=12&email=476938977&appId=ab1c
1
2
3
4
5
6
json复制代码{
"status": 500,
"message": "org.springframework.validation.BeanPropertyBindingResult: 3 errors\nField error in object 'validVO' on field 'name': rejected value [null]; codes [NotBlank.validVO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.name,name]; arguments []; default message [name]]; default message [名字为必填项]...",
"data": null,
"timestamp": 1628239301951
}
  1. 调用test3方法,提示的是javax.validation.ConstraintViolationException异常
1
2
3
4
json复制代码POST http://localhost:8080/valid/test3
Content-Type: application/x-www-form-urlencoded

email=476938977
1
2
3
4
5
6
json复制代码{
"status": 500,
"message": "test3.email: 不是一个合法的电子邮件地址",
"data": null,
"timestamp": 1628239281022
}

通过加入 Validator校验框架可以帮助我们自动实现参数的校验。

参数异常加入全局异常处理器

虽然我们之前定义了全局异常拦截器,也看到了拦截器确实生效了,但是 Validator校验框架返回的错误提示太臃肿了,不便于阅读,为了方便前端提示,我们需要将其简化一下。

直接修改之前定义的 RestExceptionHandler,单独拦截参数校验的三个异常:javax.validation.ConstraintViolationException,org.springframework.validation.BindException,org.springframework.web.bind.MethodArgumentNotValidException,代码如下:

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复制代码@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
public ResponseEntity<ResultData<String>> handleValidatedException(Exception e) {
ResultData<String> resp = null;

if (e instanceof MethodArgumentNotValidException) {
// BeanValidation exception
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
ex.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining("; "))
);
} else if (e instanceof ConstraintViolationException) {
// BeanValidation GET simple param
ConstraintViolationException ex = (ConstraintViolationException) e;
resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "))
);
} else if (e instanceof BindException) {
// BeanValidation GET object param
BindException ex = (BindException) e;
resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
ex.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining("; "))
);
}

return new ResponseEntity<>(resp,HttpStatus.BAD_REQUEST);
}

体验效果

1
2
3
4
5
6
7
8
9
java复制代码POST http://localhost:8080/valid/test1
Content-Type: application/json

{
"id": 1,
"level": "12",
"email": "47693899",
"appId": "ab1c"
}
1
2
3
4
5
6
json复制代码{
"status": 400,
"message": "名字为必填项; 不是一个合法的电子邮件地址; appId长度必须位于6到12之间",
"data": null,
"timestamp": 1628435116680
}

是不是感觉清爽多了?

自定义参数校验

虽然Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。

比如上面实体类中的sex性别属性,只允许前端传递传 M,F 这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
java复制代码@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(EnumString.List.class)
@Documented
@Constraint(validatedBy = EnumStringValidator.class)//标明由哪个类执行校验逻辑
public @interface EnumString {
String message() default "value not in enum values.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

/**
* @return date must in this value array
*/
String[] value();

/**
* Defines several {@link EnumString} annotations on the same element.
*
* @see EnumString
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {

EnumString[] value();
}
}

第二步,自定义校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class EnumStringValidator implements ConstraintValidator<EnumString, String> {
private List<String> enumStringList;

@Override
public void initialize(EnumString constraintAnnotation) {
enumStringList = Arrays.asList(constraintAnnotation.value());
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(value == null){
return true;
}
return enumStringList.contains(value);
}
}

第三步,在字段上增加注解

1
2
3
java复制代码@ApiModelProperty(value = "性别")
@EnumString(value = {"F","M"}, message="性别只允许为F或M")
private String sex;

第四步,体验效果

1
2
3
4
java复制代码POST http://localhost:8080/valid/test2
Content-Type: application/x-www-form-urlencoded

id=1&name=javadaily&level=12&email=476938977@qq.com&appId=ab1cdddd&sex=N
1
2
3
4
5
6
json复制代码{
"status": 400,
"message": "性别只允许为F或M",
"data": null,
"timestamp": 1628435243723
}

分组校验

一个VO对象在新增的时候某些字段为必填,在更新的时候又非必填。如上面的 ValidVO中 id 和 appId 属性在新增操作时都是非必填,而在编辑操作时都为必填,name在新增操作时为必填,面对这种场景你会怎么处理呢?

在实际开发中我见到很多同学都是建立两个VO对象,ValidCreateVO,ValidEditVO来处理这种场景,这样确实也能实现效果,但是会造成类膨胀,而且极其容易被开发老鸟们嘲笑。

image-20210716084136689

其实 Validator校验框架已经考虑到了这种场景并且提供了解决方案,就是分组校验,只不过很多同学不知道而已。要使用分组校验,只需要三个步骤:

第一步:定义分组接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public interface ValidGroup extends Default {

interface Crud extends ValidGroup{
interface Create extends Crud{

}

interface Update extends Crud{

}

interface Query extends Crud{

}

interface Delete extends Crud{

}
}
}

这里我们定义一个分组接口ValidGroup让其继承 javax.validation.groups.Default,再在分组接口中定义出多个不同的操作类型,Create,Update,Query,Delete。至于为什么需要继承Default我们稍后再说。

第二步,在模型中给参数分配分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Data
@ApiModel(value = "参数校验类")
public class ValidVO {
@ApiModelProperty("ID")
@Null(groups = ValidGroup.Crud.Create.class)
@NotNull(groups = ValidGroup.Crud.Update.class, message = "应用ID不能为空")
private String id;

@Null(groups = ValidGroup.Crud.Create.class)
@NotNull(groups = ValidGroup.Crud.Update.class, message = "应用ID不能为空")
@ApiModelProperty(value = "应用ID",example = "cloud")
private String appId;

@ApiModelProperty(value = "名字")
@NotBlank(groups = ValidGroup.Crud.Create.class,message = "名字为必填项")
private String name;

@ApiModelProperty(value = "邮箱")
@Email(message = "请填写正取的邮箱地址")
privte String email;

...

}

给参数指定分组,对于未指定分组的则使用的是默认分组。

第三步,给需要参数校验的方法指定分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@RestController
@Api("参数校验")
@Slf4j
@Validated
public class ValidController {

@ApiOperation("新增")
@PostMapping(value = "/valid/add")
public String add(@Validated(value = ValidGroup.Crud.Create.class) ValidVO validVO){
log.info("validEntity is {}", validVO);
return "test3 valid success";
}


@ApiOperation("更新")
@PostMapping(value = "/valid/update")
public String update(@Validated(value = ValidGroup.Crud.Update.class) ValidVO validVO){
log.info("validEntity is {}", validVO);
return "test4 valid success";
}
}

这里我们通过 value属性给 add()和 update()方法分别指定Create和Update分组。

第四步,体验效果

1
2
3
4
java复制代码POST http://localhost:8080/valid/add
Content-Type: application/x-www-form-urlencoded

name=javadaily&level=12&email=476938977@qq.com&sex=F

在Create时我们没有传递id和appId参数,校验通过。

当我们使用同样的参数调用update方法时则提示参数校验错误。

1
2
3
4
5
6
java复制代码{
"status": 400,
"message": "ID不能为空; 应用ID不能为空",
"data": null,
"timestamp": 1628492514313
}

由于email属于默认分组,而我们的分组接口 ValidGroup已经继承了 Default分组,所以也是可以对email字段作参数校验的。如:

1
2
3
4
java复制代码POST http://localhost:8080/valid/add
Content-Type: application/x-www-form-urlencoded

name=javadaily&level=12&email=476938977&sex=F
1
2
3
4
5
6
json复制代码{
"status": 400,
"message": "请填写正取的邮箱地址",
"data": null,
"timestamp": 1628492637305
}

当然如果你的ValidGroup没有继承Default分组,那在代码属性上就需要加上 @Validated(value = {ValidGroup.Crud.Create.class, Default.class}才能让 email字段的校验生效。

小结

参数校验在实际开发中使用频率非常高,但是很多同学还只是停留在简单的使用上,像分组校验,自定义参数校验这2个高阶技巧基本没怎么用过,经常出现譬如建立多个VO用于接受Create,Update场景的情况,很容易被老鸟被所鄙视嘲笑,希望大家好好掌握。

最后,我是飘渺Jam,一名写代码的架构师,做架构的程序员,期待您的转发与关注,当然也可以添加我的个人微信 jianzh5,咱们一起聊技术!

老鸟系列源码已经上传至GitHub,需要的在公号【JAVA日知录】回复关键字 0923 获取

本文转载自: 掘金

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

SpringCloudAlibaba全网最全讲解3️⃣之Na

发表于 2021-08-26

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

🌈往期回顾

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩**
  • SpringCloudAlibaba全网最全讲解2️⃣(建议收藏)
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)
  • 💖10分钟认识RocketMQ!想进阿里连这个都不会?2️⃣💖

五、服务治理:Nacos

5.1、服务治理概述

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的**自动化注册与发现**。
  1. 服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
  2. 服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。

5.2、注册中心的原理

  1. 我们再启动的时候,就会把服务的信息告诉注册中心,同时拉去一份最新的1服务列表信息在本地。
  2. 每隔一段时间就会去给注册中心发送一个心跳包,告诉注册中心我还活着,同时会拉去一份最新的服务列表信息。
  3. 如果这个服务挂了,那么他就不会再给注册中心发送心跳包了。注册中心发现连续几次都没有发送心跳包,那么注册中心就会将这个服务的节点信息给剔除掉,从而1实现动态的注册和踢出服务,就不需要我们手动管理。

image-20201029084800199

服务注册中心是微服务架构中1一个十分重要的组件,在微服务架构中11起到了一个协调者的作用,注册中心一般包含这几个功能:

  1. 服务发现:
* 服务注册:保存服务提供者和服务调用者的信息。
* 服务订阅:服务调用这订阅服务提供者的信息,注册中心向订阅者推送提供者的信息。
  1. 服务配置:
* 配置订阅:服务提供者和服务调用者订阅微服务相关的配置。
* 配置下发:主动将配置推送给服务提供者和服务调用者。
  1. 服务健康检测:检测服务提供者的健康情况,如果发现服务连续几次都没有发送心跳包,说明这个服务有异常,执行服务剔除。

5.3、常见的注册中心

5.3.1、Zookeeper

Zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

5.3.2、Eureka

Eureka是Springcloud Netflflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源

5.3.3、Consul

Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。

5.3.4、Nacos

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之一,负责服务注册发现和服务配置。

5.4、Nacos简介

image-20210505183124825

Nacos是阿里巴巴2018年7月推出来的一个开源项目,是一个更易于构建云原生应用的动态服务注册与发现、配置管理和服务管理平台。Nacos致力于快速实现动态服务注册与发现、服务配置、服务元数据及流量管理。


他的核心功能:
  1. 服务注册:
Nacos Client会通过发送REST请求想Nacos Server注册自己的服务,提供自身的元数据,比如IP地址,端口等信息。Nacos Server接收到注册请求后,就会把这些元数据存储到一个双层的内存Map中。
  1. 服务心跳:
在服务注册后,Nacos Client会维护一个定时心跳来维持统治Nacos Server,说明服务一致处于可用状态,防止被剔除,默认5s发送一次心跳。
  1. 服务同步:
Nacos Server集群之间会相互同步服务实例,用来保证服务信息的一致性。
  1. 服务发现:
服务消费者(Nacos Client)在调用服务提供的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务拉取服务最新的注册表信息更新到本地缓存。
  1. 服务健康检查:
Nacos Server 会开启一个定时任务来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将他的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。

5.5、Nacos实战

5.5.1、搭建Nacos环境

5.5.1.1、下载Nacos环境

我们需要去[下载地址](https://github.com/alibaba/nacos/releases)下载Nacos,下载的Nacos是zip格式的安装包,然后进行解压缩操作。如果在Linux的话需要执行:
1
shell复制代码tar -zxvf Nacos下载包的名称

5.5.1.2、启动

我们以Windows下为例,Linux下也是一样的。我们直接切换到bin目录下,然后cmd,输入命令:
1
shell复制代码startup.cmd -m standalone
**单机环境必须带-m standalone参数启动,否则无法启动,不带参数启动的是集群环境**。


我们不带参数(以集群方式启动),会出现下列错误。

image-20210505184150833

正确启动:

image-20210505184235392

5.5.1.3、测试

打开浏览器输入<http://localhost:8848/nacos/index.html#/login> ,即可访问服务,默认账号密码都是nacos。

image-20210505184636822

5.5.2、将商品服务注册到Nacos

5.5.2.1、添加依赖

我们需要在shop-product-server模块中的pom.xml文件中添加nacos的依赖。
1
2
3
4
5
xml复制代码<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

5.5.2.2、在主类上添加注解

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableDiscoveryClient
public class ShopProductServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopProductServerApp.class,args);
}
}

5.5.2.3、添加Nacos服务地址

我们需要在application.yml中添加nacos服务的地址。
1
2
3
4
5
yaml复制代码spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848

5.5.2.4、查看

image-20210505185212140

说明注册成功!

5.5.3、将订单服务注册到Nacos

接下来开始修改 shop-order-server 模块的代码, 将其注册到nacos服务上

5.5.3.1、添加依赖

1
2
3
4
5
xml复制代码<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

5.5.3.2、添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@SpringBootApplication
@EnableDiscoveryClient
public class ShopOrderServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopOrderServerApp.class,args);
}

@Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}
}

5.5.3.3、添加Nacos服务地址

1
2
3
4
5
yaml复制代码spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848

5.5.3.4、测试

image-20210505185547641

5.5.4、编写测试代码

1
2
3
4
5
6
7
java复制代码  @Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("test")
public String test(){
ServiceInstance serviceInstance = discoveryClient.getInstances("product-service").get(0);
return serviceInstance.toString();
}

image-20210505191627907

本文转载自: 掘金

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

1…548549550…956

开发者博客

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