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

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


  • 首页

  • 归档

  • 搜索

SpringBoot 拦截器妙用,让你一个人开发整个系统的鉴

发表于 2021-07-01

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战」

我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。

HandlerInterceptor 详解

HandlerInterceptor 允许定制 handler 处理器执行链的工作流接口。我们可以自定义拦截器用于拦截 handlers 处理器(你可以理解为 controller 层的接口),从而可以添加一些共同的重复性的处理行为(例如接口鉴权,接口日志记录,性能监控等),而不用修改每一个 handler 的实现。

注意,此基于 SpringBoot 2.3.12.RELEASE 版本讲解。

HandlerInterceptor 接口只有三个默认空实现方法,在低版本中这三个方法不是默认方法,而是抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface HandlerInterceptor {

default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}

default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}

default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

这三个方法的执行顺序图如下:

image-20210630225337581.png

preHandle

preHandle 前置处理,拦截一个处理器(handler)的执行,preHandle 方法会在 HandlerMapping 确定一个适当的处理器对象之后,但在 HandlerAdapter 调用处理器之前被调用。可以简单理解为 controller 接口被调用之前执行。

Intercepter 是链式的,就是一个接着一个执行。如果此方法返回 true,则会执行下一个拦截器或者直接执行处理器。如果此方法返回 false 或者抛出异常则终止执行链,也不再调用处理器。

注意,此方法如果不返回 true,那么 postHandle 和 afterCompletion 不会被执行。

那这个方法有什么用呢?其实可以做一些接口被调用前的预处理,例如用户权限校验。

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复制代码package com.chenpi;

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

import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
* @Description 用户权限验证拦截
* @Author 陈皮
* @Date 2021/6/27
* @Version 1.0
*/
@Component
public class UserPermissionInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {

if (handler instanceof HandlerMethod) {

HandlerMethod handlerMethod = (HandlerMethod) handler;

// 获取用户权限校验注解
UserAuthenticate userAuthenticate =
handlerMethod.getMethod().getAnnotation(UserAuthenticate.class);
if (null == userAuthenticate) {
userAuthenticate = handlerMethod.getMethod().getDeclaringClass()
.getAnnotation(UserAuthenticate.class);
}
if (userAuthenticate != null && userAuthenticate.permission()) {
// 验证用户信息
UserContext userContext = userContextManager.getUserContext(request);
if (null == userContext) {
return false;
}
}
}
return true;
}
}

postHandle

postHandle 后置处理,会在 HandlerAdapter 调用处理器之后,但在 DispatcherServlet 渲染视图之前被调用。可以在此对 ModelAndView 做一些额外的处理。可以简单理解为 controller 接口被调用之后执行。

注意,此方法在执行链中的执行顺序是倒着执行的,即先声明的拦截器后执行。

afterCompletion

afterCompletion 完成之后,在请求处理完之后被执行,也就是渲染完视图之后。一般用于做一些资源的清理工作,配合 preHandle 计算接口执行时间等。

注意,和 postHandle 一样,此方法在执行链中的执行顺序也是倒着执行的,即先声明的拦截器后执行。

1
2
3
4
5
6
java复制代码@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable Exception ex) {
// 请求完后,清除当前线程的用户信息
UserContextHolder.removeUserContext();
}

注册拦截器

注意,我们自定义的拦截器要通过 WebMvcConfigurer 的实现类进行注册,才能生效。

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复制代码package com.yzj.ehr.common.config;

import com.yzj.ehr.common.context.UserContextResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.yzj.ehr.common.interceptor.UserPermissionInterceptor;

/**
* @Description 注册拦截器
* @Author 陈皮
* @Date 2021/6/27
* @Version 1.0
*/
@Component
public class WebAppConfigurer implements WebMvcConfigurer {

private UserPermissionInterceptor userPermissionInterceptor;

public WebAppConfigurer(final UserPermissionInterceptor userPermissionInterceptor) {
this.userPermissionInterceptor = userPermissionInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 匹配所有接口,排除/base/test接口
registry.addInterceptor(userPermissionInterceptor).addPathPatterns("/**")
.excludePathPatterns("/base/test");
}
}

本文转载自: 掘金

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

⼀条SQL语句执⾏得很慢的原因有哪些?

发表于 2021-07-01

一、分类讨论

一条 SQL 语句执行的很慢,那是每次执行都很慢呢?还是大多数情况下是正常的,偶尔出现很慢呢?所以我觉得,我们还得分以下两种情况来讨论。

  1. 大多数情况是正常的,只是偶尔会出现很慢的情况。
  2. 在数据量不变的情况下,这条SQL语句一直以来都执行的很慢。

针对这两种情况,我们来分析下可能是哪些原因导致的。

二、针对偶尔很慢的情况

一条 SQL 大多数情况正常,偶尔才能出现很慢的情况,针对这种情况,我觉得这条SQL语句的书写本身是没什么问题的,而是其他原因导致的,那会是什么原因呢?

1、数据库在刷新脏页(flush)我也无奈啊

当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。

当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

刷脏页有下面4种场景(后两种不用太关注“性能”问题):

redolog写满了:

redo log 里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候 redo log 很快就会被写满了,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中去的,而这个时候,就会导致我们平时正常的SQL语句突然执行的很慢,所以说,数据库在在同步数据到磁盘的时候,就有可能导致我们的SQL语句执行的很慢了。

内存不够用了:

如果一次查询较多的数据,恰好碰到所查数据页不在内存中时,需要申请内存,而此时恰好内存不足的时候就需要淘汰一部分内存数据页,如果是干净页,就直接释放,如果恰好是脏页就需要刷脏页。

MySQL 认为系统“空闲”的时候:

这时系统没什么压力。

MySQL 正常关闭的时候:

这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

2、拿不到锁我能怎么办

这个就比较容易想到了,我们要执行的这条语句,刚好这条语句涉及到的表,别人在用,并且加锁了,我们拿不到锁,只能慢慢等待别人释放锁了。或者,表没有加锁,但要使用到的某个一行被加锁了,这个时候,我也没办法啊。

如果要判断是否真的在等待锁,我们可以用 show processlist这个命令来查看当前的状态哦,这里我要提醒一下,有些命令最好记录一下,反正,我被问了好几个命令,都不知道怎么写,呵呵。

下来我们来访分析下第二种情况,我觉得第二种情况的分析才是最重要的

三、针对一直都这么慢的情况

如果在数据量一样大的情况下,这条 SQL 语句每次都执行的这么慢,那就就要好好考虑下你的 SQL 书写了,下面我们来分析下哪些原因会导致我们的 SQL 语句执行的很不理想。

我们先来假设我们有一个表,表里有下面两个字段,分别是主键 id,和两个普通字段 c 和 d。

1
2
3
4
5
6
sql复制代码mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
1、扎心了,没用到索引

没有用上索引,我觉得这个原因是很多人都能想到的,例如你要查询这条语句

select * from t where 100 <c and c < 100000;

a. 字段没有索引

刚好你的 c 字段上没有索引,那么抱歉,只能走全表扫描了,你就体验不会索引带来的乐趣了,所以,这回导致这条查询语句很慢。

b. 字段有索引,但却没有用索引

好吧,这个时候你给 c 这个字段加上了索引,然后又查询了一条语句

select * from t where c - 1 = 1000;
我想问大家一个问题,这样子在查询的时候会用索引查询吗?

答是不会,如果我们在字段的左边做了运算,那么很抱歉,在查询的时候,就不会用上索引了,所以呢,大家要注意这种字段上有索引,但由于自己的疏忽,导致系统没有使用索引的情况了。

正确的查询应该如下

select * from t where c = 1000 + 1;

有人可能会说,右边有运算就能用上索引?难道数据库就不会自动帮我们优化一下,自动把 c - 1=1000 自动转换为 c = 1000+1。

不好意思,确实不会帮你,所以,你要注意了。

c. 函数操作导致没有用上索引

如果我们在查询的时候,对字段进行了函数操作,也是会导致没有用上索引的,例如

select * from t where pow(c,2) = 1000;
这里我只是做一个例子,假设函数 pow 是求 c 的 n 次方,实际上可能并没有 pow(c,2)这个函数。其实这个和上面在左边做运算也是很类似的。

所以呢,一条语句执行都很慢的时候,可能是该语句没有用上索引了,不过具体是啥原因导致没有用上索引的呢,你就要会分析了,我上面列举的三个原因,应该是出现的比较多的吧。

2、呵呵,数据库自己选错索引了

我们在进行查询操作的时候,例如

select * from t where 100 < c and c < 100000;
我们知道,主键索引和非主键索引是有区别的,主键索引存放的值是整行字段的数据,而非主键索引上存放的值不是整行字段的数据,而且存放主键字段的值。不大懂的可以看这篇文章: 【思维导图-索引篇】搞定数据库索引就是这么简单 里面有说到主键索引和非主键索引的区别

也就是说,我们如果走 c 这个字段的索引的话,最后会查询到对应主键的值,然后,再根据主键的值走主键索引,查询到整行数据返回。

好吧扯了这么多,其实我就是想告诉你,就算你在 c 字段上有索引,系统也并不一定会走 c 这个字段上的索引,而是有可能会直接扫描扫描全表,找出所有符合 100 < c and c < 100000 的数据。

为什么会这样呢?

其实是这样的,系统在执行这条语句的时候,会进行预测:究竟是走 c 索引扫描的行数少,还是直接扫描全表扫描的行数少呢?显然,扫描行数越少当然越好了,因为扫描行数越少,意味着I/O操作的次数越少。

如果是扫描全表的话,那么扫描的次数就是这个表的总行数了,假设为 n;而如果走索引 c 的话,我们通过索引 c 找到主键之后,还得再通过主键索引来找我们整行的数据,也就是说,需要走两次索引。而且,我们也不知道符合 100 c < and c < 10000 这个条件的数据有多少行,万一这个表是全部数据都符合呢?这个时候意味着,走 c 索引不仅扫描的行数是 n,同时还得每行数据走两次索引。

所以呢,系统是有可能走全表扫描而不走索引的。那系统是怎么判断呢?

判断来源于系统的预测,也就是说,如果要走 c 字段索引的话,系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多,它可能就不走索引而直接扫描全表了。

那么问题来了,系统是怎么预测判断的呢?这里我给你讲下系统是怎么判断的吧,虽然这个时候我已经写到脖子有点酸了。

系统是通过索引的区分度来判断的,一个索引上不同的值越多,意味着出现相同数值的索引越少,意味着索引的区分度越高。我们也把区分度称之为基数,即区分度越高,基数越大。所以呢,基数越大,意味着符合 100 < c and c < 10000 这个条件的行数越少。

所以呢,一个索引的基数越大,意味着走索引查询越有优势。

那么问题来了,怎么知道这个索引的基数呢?

系统当然是不会遍历全部来获得一个索引的基数的,代价太大了,索引系统是通过遍历部分数据,也就是通过采样的方式,来预测索引的基数的。

扯了这么多,重点的来了,居然是采样,那就有可能出现失误的情况,也就是说,c 这个索引的基数实际上是很大的,但是采样的时候,却很不幸,把这个索引的基数预测成很小。例如你采样的那一部分数据刚好基数很小,然后就误以为索引的基数很小。然后就呵呵,系统就不走 c 索引了,直接走全部扫描了。

所以呢,说了这么多,得出结论:由于统计的失误,导致系统没有走索引,而是走了全表扫描,而这,也是导致我们 SQL 语句执行的很慢的原因。

这里我声明一下,系统判断是否走索引,扫描行数的预测其实只是原因之一,这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。

不过呢,我们有时候也可以通过强制走索引的方式来查询,例如

select * from t force index(a) where c < 100 and c < 100000;
我们也可以通过

show index from t;
来查询索引的基数和实际是否符合,如果和实际很不符合的话,我们可以重新来统计索引的基数,可以用这条命令

analyze table t;
来重新统计分析。

既然会预测错索引的基数,这也意味着,当我们的查询语句有多个索引的时候,系统有可能也会选错索引哦,这也可能是 SQL 执行的很慢的一个原因。

好吧,就先扯这么多了,你到时候能扯出这么多,我觉得已经很棒了,下面做一个总结。

四、总结

以上是我的总结与理解,最后一个部分,我怕很多人不大懂数据库居然会选错索引,所以我详细解释了一下,下面我对以上做一个总结。

一个 SQL 执行的很慢,我们要分两种情况讨论:

1、大多数情况下很正常,偶尔很慢,则有如下原因
  • 数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。
  • 执行的时候,遇到锁,如表锁、行锁。
2、这条 SQL 语句一直执行的很慢,则有如下原因。
  • 没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。
  • 数据库选错了索引。

本文转载自: 掘金

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

如何在二三线城市月薪过万(一)看完这篇后端简历优化,包你面试

发表于 2021-07-01

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

楼主 在职场生涯中,大概也面试过100位左右后端面试者,通过楼主的主观感受和经验,总结了下列内容。

1 个人介绍

  1. 如果可能使用不要使用qq邮箱,使用163邮箱,这样显得是技术大佬,因为大佬大多数都不走寻常路。

  1. 如果不是党员,团员少先队员就不用写了。。。。(30多岁都自动退团了)

  1. 如果非计算机专业,就别写了,感觉你这样让面试官注意你非科班出身,如果不写可能不会注意。

2 个人评价

  1. 不要写太多,什么性格吃苦耐劳,活泼大方,这就不要特别描述了,你是不是这样我也不知道。。。。也不关注,但是可以适当表现出你想在公司长期干的意愿(招聘成本很大),或者是之前的工作经历,或者是内卷绝招!!!!能加班能出差(百分之80能过)

一般简历

一堆套话,大家都这么写,看着毫无新意。而且具体是否符合描述,看不出来。

突出简历

描述出自己会多种语言,而且担任过项目leader。在问答过程中能够辨认是否副实。而且我认为公司是需要有这种能力的员工。

3 爱好

  1. 能少些就少些(不限于有关于计算机方面,如贡献开源,写技术博客),如果没有以上不写更好。。。。切记 千万别写王者荣耀等。。。。ps:你啥都爱好还有时间干活了吗。。。 如下图这位,我在想你是否有空加班。。。

4 学校情况

  1. 如果你是社招,请不要把学生会主席什么的都写出来,我会认为你在工作过程中没有亮点!!!

5 教育经历

这个正常写吧 ,做不了假,科班,本科加分,但是不是硬指标。

科班培训过的应届生加分

大培训机构如:北京黑马等机构加分
重头戏来了,有关于技术栈的书写,请注意:

6 技术栈

  1. 不要写精通,不要写精通,不要写精通,你写精通我能让你有100种方式不精通。(本条不适于大佬)
  2. 不会的,不熟的不要写,大多数都是按照简历问的,不要给自己挖坑。
  3. 有以下经验要写最显眼地方,加分项!!!(没有别编….)
1
2
3
4
5
6
7
复制代码1.xxx博客优质作者,有xxx篇原创博客。
2.了解xxx底层原理,阅读过xxx技术源码。
3.有过管理经验,管理过xxx人团队。
4.致力于开源,是xxx开源项目贡献者。
5.github上有xx开源项目,地址为xxxx
6.有过xxx高并发经验
7.有过xx集群经验
  1. 细化技术栈

一般简历

太过大众化。多数简历都是这种,没有突出亮点。

优秀简历

能够清楚掌握程度。同时对比其他人,有这具体功能的描述,让面试官认知到你确实使用过,而不是了解就写精通的那种面试者。

总之可以详细介绍你对此技术了解到了什么程度,但是特别特别简单的不要写,要突出亮点项

7 项目介绍

  1. 在项目介绍时 要介绍项目功能,使用什么技术,最重要的是你在项目中担任什么职位!!!!!如:java后端分端负责人,项目负责人(这些加分)。在项目中干了什么!!(如:与产品与测试对接需求经历,为组员提供技术经历,核心通用方法编写经历,rpc调用经历)都为加分项,千万不要只介绍项目,然后你是干什么的不写,项目再大于你也没有关系

职位加分描述:除了底层coder都是加分项,如:后端负责人,前端负责人,架构师,组长。。。。。。

  1. 实现功能加分描述:除了xxx模块的增删改查业务都是加分项

如:表设计,代码review测试,为他人提供公用方法,文档整理,人脸识别等听起来高端的sde调用,rpc调用,项目设计,人员管理,工作量分配,需求对接,大厂工作经验,高并发经验,集群搭建经验,linux操作经验,一线中间件的操作搭建经验等。。。。。

一般简历

只是描述了项目时干什么的,使用了什么技术,而你在项目中是什么角色并没有表达出来。

突出简历

描述面试者在项目中角色,不是让我在猜这里有什么亮点。同时了解了面试者并不只是curd程序猿。即使是简单的上传下载,或者公共接口调用,也比curd强的多。

最后 祝大家找到合适的工作!

本文转载自: 掘金

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

全网最容易理解的spring dispatcher请求流程解

发表于 2021-07-01

servlet

在了解dispatcherServlet的流程前,首先要了解servlet这个技术。

狭义的来说,servlet只是一个接口,广义的来说,他是一个规范。

在传统的Javaweb项目里,通过继承HttpServlet(实现了service接口)重写doGet,doPost方法,再在web.xml里标识。通过tomcat作为servlet容器来管理这些servlet。

tomcat在不考虑io模型通信情况下,只有两个主要的功能。

  • 封装request,response对象
  • 调用servlet的service方法

这个是理解dispatcher流程的关键,首先要知道tomcat到底是干什么的

我们可以看下HttpServlet的service方法的源码

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
Java复制代码protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}

}

也就是说传统的servlet的项目中,tomcat把请求封装后根据url调用对应路径的servlet,执行service方法,再判断请求类型去调用doGet(),doPost()等。

dispatcherServlet和servlet的关系

dispatcherServlet依赖关系很复杂,简单来说就是他间接的实现了servlet接口。先看下servlet接口的方法

1
2
3
4
5
6
7
8
9
10
11
Java复制代码public interface Servlet {
void init(ServletConfig var1) throws ServletException;

ServletConfig getServletConfig();

void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

String getServletInfo();

void destroy();
}

我们一会重点说下init和service方法。

对于springmvc来说,需要在web.xml里配置dispatcherServlet,让tomcat去管理。对于spring的项目,tomcat只需要管理spring自带的servlet就可以了,我们不需要去写servlet,我们是通过bean让spring容器去管理。这里的spring容器和tomcat容器不是一个概念。

springboot基于自动配置不需要去手动配置。

根据上面所说,任何路径下的请求,tomcat都会调用spring的dispatcherServlet的service方法去处理,由于依赖关系复杂,service方法是在dispatcher的父类。总结来说,最后会调用dispatch的doDispatch方法,中间会有些过程,这里就忽略不计了。

流程

丑陋的面经只会甩给你一张图

img

硬背这种八股文,完全不知道他的理念和实现细节,对我们的思想是没有一点好处的。所以要剖析下他的源码。

初始化

首先要对spring容器初始化刷新容器,才能进行交互。对于springmvc来说,tomcat会先执行dispatcher的init方法(其实是HttpServletBean的init方法)来刷新容器,底层就是调用的refresh方法。对于springboot来说,会执行main方法的run方法,里面执行一个refresh方法来刷新容器。虽然仍然会执行init方法,但里面的refresh方法应该是不会再执行的。(虽然不知道springboot为什么可以不让执行,但根据打断点来说,他确实没有执行)。总之殊途同归。

请求流程

我再把之前的捋一遍,请求过来后,先被tomcat封装request,response对象,在调用service方法,即dispatchServlet的doDispatch方法,直接看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
Java复制代码protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

这么看复杂的一比,实际上对比丑陋面经的图片,只有几个方法比较重要。

1
2
3
4
5
6
7
8
9
10
java复制代码//从Handler映射器获取handlerchain
mappedHandler = getHandler(processedRequest);
//获取适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
//执行handler,获取ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//判断mv中的视图是否为空
applyDefaultViewName(processedRequest, mv);
//视图解析
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

感觉这应该是spring中最容易理解的源码了,我们逐条分析。

映射器

首先是getHandler()方法

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码	protected HandlerExecutionChain getHandler(HttpServletRequest request) {
//this.handlerMappings就是一个HandlerMapping数组,Spring容器启动后,会将所有定义好的映射器对象存放在这个数组里
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

这个理解起来很简单,由于有多种的Handler,就有多种的mapping,需要找到正确的mapping

  • RequestMappingUrlHandlerMapping 最常见的使用注解标识的handler映射
  • BeanNameUrlHandlerMapping
  • SimpleUrlHandlerMapping
  • ControllerClassNameHandlerMapping

其中第一个最为常见,使用@requestMapping注解标识的handlermapping,其他的有继承controller,httpservlet的,就不多说了。

通过循环,直到找到正确的处理器映射器,获取到handle,实际上是一个HandlerExecutionChain,他的构成其实就是拦截器数组+handler。

适配器

上一步获取到了处理器链后,获取适配器,通过getHandlerAdapter方法,参数为handler(目前拦截器数组还没有用)

1
ini复制代码HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

这里就用上了大名鼎鼎的适配器模式了

如果不使用适配器,以后再增加handle种类时,由于不同handle之间的逻辑完全不一样,需要通过在dispatch中使用if else的方式去判断handle的种类,再执行操作。

1
2
3
4
5
6
7
8
9
java复制代码	protected HandlerAdapter getHandlerAdapter(Object handler) {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
};
}

这里的this.handlerAdapters是获取适配器数组,跟上面有点像。获取到合适的适配器返回。

适配器执行handle方法

适配器使用handle方法执行

1
ini复制代码mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

最后得到了一个modelandview。在古老的jsp时代,是通过modelandview传递数据并解析jsp视图的。但由于现在前后端分离,使用@ResponseBody注释只传送json串,所以这里的modelandview为空。

所以接下来的视图解析部分我们就不关注了

拦截器

所以handerchain里的拦截器作用是什么呢。

其实在适配器执行handle方法前后会进行一个拦截器的处理,只是我没有写而已。

最后

tomcat的处理,我就不说了

小结

springmvc这块的流程如果不深挖其实很简单,基本上有过开发经验的都能够大概了解这块的流程。但其实我上面省略了一部分,就是适配器的handle方法和映射器的getHandler方法。这块比较难理解,所以我这里只介绍前后端分离和常用注解的情况,我尽量从顶向下的方式去说明源码。

映射器的源码分析

面经简略版

  1. 在ioc初始化时,映射器也会初始化,就把映射对应关系放在一个map里
  2. 映射器的getHandler方法,通过url的后缀,从map中获取到对应的handler

详细版

先说下HandlerMapping的继承关系

实际上最常用的就是RequestMappingHandlerMapping ,他对应的就是@Controller @RequestMapping这种写法的handle映射器。我这里就只介绍这一个映射器了。

我们直接从请求流程来看

首先先是在dispatcher里循环映射器列表,调用getHandler方法(上面有写),实际就是调用AbstractHandlerMapping的getHandler方法

AbstractHandlerMapping

1
2
3
4
5
6
java复制代码public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//省略版
Object handler = getHandlerInternal(request);
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
return executionChain;
}

这个getHandlerInternal()返回的就是我们需要的handle,点进去会调用AbstractHandlerMethodMapping的方法

getHandlerInternal()

1
2
3
4
5
6
7
java复制代码protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//获取url,例如/user/login
String lookupPath = initLookupPath(request);
//获取HandlerMethod,核心方法
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}

先看返回值,返回了一个HandlerMethod,这个就是我们的处理器。之前说过,springmvc有多种handler,每个handle的处理逻辑都不一样,通过不同映射器获得的handler都不同,之后需要通过适配器来统一规范执行。这个后面再说,总之HandlerMethod这个对象很重要。

HandlerMethod类

看下HandlerMethod类的构造

1
2
3
4
5
6
7
java复制代码//省略了很多
public class HandlerMethod {

private final Object bean;

private final Method method;
}

就介绍下这两个成员变量吧。bean就是controller的bean,Method是映射到controller的方法,Method不懂的话可以去看下反射的原理。总之在后面适配器那里会执行method的invoke方法,也就是执行controller上映射的方法。

我们在返回刚才的方法,看下核心方法lookupHandlerMethod

lookupHandlerMethod()

1
2
3
4
5
6
7
8
9
java复制代码	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
//重点
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
Match bestMatch = matches.get(0);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}

这个代码我进行了疯狂的省略,把一些多映射或者空的情况删掉了,我们先去理解正常代码的一个流程。

我们来分析下第二行重点代码,this.mappingRegistry.getMappingsByDirectPath(lookupPath),我们首先要看这个mappingRegistry是什么。

mappingRegistry类

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class MappingRegistry {

private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();

private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
}

mappingRegistry是一个内部类,里面都是一堆map。这个getMappingsByDirectPath方法只是从pathLookup这个map里,以lookupPath(url,例如/user/login)为key获取值而已,这个值就是我们的handler。

这时候问题就来了,这些map是什么时候获取的内容呢

img

在springioc容器初始化时,映射器也会初始化,就把映射对应关系放在pathLookup这个map里。具体流程就跟容器初始化有关了,我就不详细叙述了。

小结

可以发现,对于这种请求来说,根本就没用到RequestMappingHandlerMapping的方法,都是他的父类AbstractHandlerMapping的方法。之后的流程就很简单了,只是把handlerMethod封装成handerchain,返回了。

适配器的源码分析

面经简略版

  • 调用RequestMappingHandlerAdapter的handle方法,实际是通过反射调用controller的方法,并返回值
  • 根据返回值选择特定的返回值解析器(例如使用@Response注解返回的是对象,则使用RequestResponseBodyMethodProcessor,将对象序列化为json)
  • 最后返回的mv为null,跳过了视图解析器。

详细版

HandlerAdapter

上面流程的模块介绍了适配器有很多种,他们都实现了HandleAdapter接口,看下接口的源码

1
2
3
4
5
java复制代码public interface HandlerAdapter {
boolean supports(Object handler);

ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
}

同样省略,重要的只有这两个方法。

第一个方法在上文循环判断是否合适时调用过,第二个方法就是dispatcher里的执行方法,返回一个modelAndView。这么一看很简单吗,所以我们来看下他的实现类。

由于handle种类很多,就会对应了很多的适配器

img

我们平常@Controller出来的HandlerMethod使用的是RequestMappingHandlerAdapter,是这里最复杂的。为什么复杂呢,因为他涉及了很多的url映射,参数和返回值的处理。这里我先举一个简单的例子,SimpleServletHandlerAdapter,servlet类型的handler适配器。

SimpleServletHandlerAdapter

1
2
3
4
5
6
7
8
9
10
typescript复制代码public class SimpleServletHandlerAdapter implements HandlerAdapter {
public boolean supports(Object handler) {
return (handler instanceof Servlet);
}

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
((Servlet) handler).service(request, response);
return null;
}
}

这里先说明一点,spring也是可以使用servlet规范的写法的,例如继承httpservlet,只是还会走mvc的流程,因为这种servlet不归tomcat管理,而是作为一个bean被spring容器管理。

可以看到这个实现类和他的名字一样简单。supports只是判断了handler是否是servlet类型,handle只是调用了service。

AbstractHandlerMethodAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//简化版
public abstract class AbstractHandlerMethodAdapter implements HandlerAdapter {
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}

protected abstract boolean supportsInternal(HandlerMethod handlerMethod);

public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler){
return handleInternal(request, response, (HandlerMethod) handler);
}

protected abstract ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
}

先看RequestMappingHandlerAdapter的抽象类。

  • supports判断类型是否是HandlerMethod,除此之外还有个新方法supportsInternal,但是对于RequestMappingHandlerAdapter来说,这个方法一直返回true。所以我们可以认为, 当一个Handler是HandlerMethod 类型的时候, 就会被这个适配器处理。这里的重点就是HandlerMethod,他代表这我们平常使用的handler类型
  • handle调用新方法handleInternal,作为参数的handler被强转为HandlerMethod类型

handle方法流程

刚才简单介绍下父类适配器的接口,接下来看流程

上面说到,dispatcher会先循环获取对应的handleAdapeter,通过的就是supports方法,我就忽略掉了。

然后会调用handle方法,其实就是RequestMappingHandlerAdapter的handleInternal方法

RequestMappingHandlerAdapter

handleInternal()

1
2
3
4
5
java复制代码protected ModelAndView handleInternal(){
ModelAndView mav;
mav = invokeHandlerMethod(request, response, handlerMethod);
return mav;
}

在抽象的省略后,只剩下invokeHandlerMethod方法

invokeHandlerMethod()

1
2
3
4
5
6
7
8
9
10
11
java复制代码protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
//ServletInvocableHandlerMethod是HandlerMethod的子类
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
//核心
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}

这里有大量异步和mavcontainer的部分我都删掉了,没意义的同时也是因为我看不懂,我就不分析了。

这块代码核心就是invocableMethod.invokeAndHandle(webRequest, mavContainer),分析下参数。

  • webRequest request的封装
  • mavcontainer在前后端分离情况都是为空的。

HandlerMethod子类

1
2
3
4
5
6
7
8
java复制代码	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//获得返回值,providedArgs为参数
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//返回值解析
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
1
2
3
4
5
6
java复制代码public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//参数解析器,例如@RequestBody,对象,变量之类的,对参数解析之后放入args数组,
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
return doInvoke(args);
}
1
2
3
4
5
6
java复制代码protected Object doInvoke(Object... args) throws Exception {
//获取method,这个method就是controller对应url的方法
Method method = getBridgedMethod();
//反射调用controller的方法
return method.invoke(getBean(), args);
}

这是请求到controller之前的最后一步,这是InvocableHandlerMethod的doInvoke方法。这里只进行了一个简单的反射而已。获取到Method对象后调用invoke方法。也就是执行controller下映射url的方法,我这里举一个例子

1
2
3
4
5
java复制代码@GetMapping
public ApiMsg get(@RequestParam(value = "current", required = false, defaultValue = "1") int current,
@RequestParam(value = "size", required = false, defaultValue = "10") int size){
return adminReportService.selectAllReport(current,size);
}

一个极其标准的controller,返回的是一个Object对象

对于返回值和参数值的解析我就不研究了。

小结

对于前后端分离传递json这种写法,modelandview都为空,都是直接对返回值处理的,我就没有关注这个modelandview的处理。

总结

作为最常使用的mvc,应该是对于我们这种web开发来说最容易理解的spring源码了。里面还有很多的学问,包括对不同handler的处理或者不同类之间的区别,jsp的解析等,我才疏学浅就不深究了。如果上面有什么写的不对,请评论告诉我。

本文转载自: 掘金

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

PC微信机器人接口api之实战分析微信好友群列表hook地址

发表于 2021-07-01

本节主要讲一下或者所有微信好友群列表的hook地址,我们的思路是,在好友列表,点击某一个人,然后复制这个人的微信号,注意找没有设置过微信号的好友,此时他的微信号就是微信id,然后把这个好友的微信id,在CE里面搜索,

image.png
然后切换另外一个好友,再搜索微信id,剩余没几个值了,然后把这几个值,移到下面。

image.png
然后把这剩余的四个地址,在OD里面一个一个试,在OD里面下一个内存写入断点,

image.png
然后在左边找到个人的一些信息,右键,在反汇编窗口跟踪,在汇编窗口下一个断点,然后点击运行,再在打开微信窗口,切换个微信好友,这个断点断下来了,然后再点击运行,此时让好友列表刷新一下,就删除一个好友,删除后,再看看这个断点是否断下,如果没有断下,说明就好友列表就没有走这个断点,就不是我们需要的,此时在CE里面再换一个地址,同样的步骤,再试一次,删除后,可以端下来,就说明可能是这个call。然后先把这个断点保存,从新登陆一下微信,加载好友列表,看是否断下,

image.png
然后登陆后,发现该断点断下了,然后单步走一下,看看里面是不是好友列表的数据,然后发现一直循环断下,里面也都是微信好友的数据,此时说明这个地址就是我们要找的call。

image.png
目前已经实现了大部分功能,运行稳定,比如:发各种消息,
接收各种消息,群管,下载文件,加好友,检测僵尸粉等等功能,
可提供接口,方便各种语言二次开发,欢迎技术交流,请勿用于商业用途。

有联系人新版.jpg

本文转载自: 掘金

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

物联网RTU(Modbus TCP协议)Java接口开发及M

发表于 2021-07-01

  在物联网体系中,经常用到RTU(远程终端单元),RTU是负责对现场信号、工业设备的监测和控制,通常由信号输入/出模块、微处理器、有线/无线通讯设备、电源及外壳等组成,由微处理器控制,并支持网络系统。

  在物联网应用平台上,需要通过RTU与现场设备对接,采集现场数据、控制设备,通过网络与RTU连接,主要是采用Modbus TCP协议。

  Modbus是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一种通用工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。此协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一个控制器请求访问其它设备的过程,如何回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。

  物联网应用软件开发,需要与RTU接口,为了方便软件开发,降低对物联网设备的依赖,Modbus仿真软件也就出现,对此,物联网开发人员应该很熟悉,但是对于普通互联网开发人员,还是很陌生的,为此,此文介绍ModbusTCP仿真软件使用和基于Netty的RTU java接口开发。

1、ModbusTCP仿真

  ModbusSlave(软件官方网站地址)是一个从站设备仿真软件,它用于接收主设备的命令包,并回送数据包;可用于测试和调试Modbus主站设备,便于观察Modbus通信过程中的各种报文。ModbusPoll及ModbusSlave支持ModbusRTU, ASCII,TCP/IP等协议。

  首先,了解MODBUS支持的部分功能代码,以十进制表示,如下表所示。

代码 中文名称 英文名称 位操作/字操作 操作数量
01 读线圈状态 READ COIL STATUS 位操作 单个或多个
02 读离散输入状态 READ INPUT STATUS 位操作 单个或多个
03 读保持寄存器 READ HOLDING REGISTER 字操作 单个或多个
04 读输入寄存器 READ INPUT REGISTER 字操作 单个或多个
05 写线圈状态 WRITE SINGLE COIL 位操作 单个
06 写单个保持寄存器 WRITE SINGLE REGISTER 字操作 单个
15 写多个线圈 WRITE MULTIPLE COIL 位操作 多个
16 写多个保持寄存器 WRITE MULTIPLE REGISTER 字操作 多个

  参数设置:
  点击菜单“Setup”中“Slave Definition.. F2”进行参数设置,会弹出如下图对话框

这里写图片描述

  其中:
  (1)Slave ID:设备ID;
  (2)Function:对应上表所对应的Modbus功能,例如本文所选用的“03 Holding Register…”,与下文Java代码“见类ReadHoldingRegistersResponse ”所对应。
  (3)Address:寄存器地址;
  (4)Quantity:数量。

  打开ModbusSlave软件,为方便起见,本文采用默认的地址(localhost,与下文第二段代码对应“见类ClientForTests ”),功能码,寄存器数量,单击Connection->connect,在弹出的窗口设置connection为TCP/IP,端口Port设置为30502,点击OK,如下图所示,从端配置完毕。

这里写图片描述

  注意:
  (1)本文连接Connection采用Modbus TCP/IP协议;
  (2)网络地址为本地地址,127.0.0.1;
  (3)端口与下文第二段代码“见类ClientForTests”中的地址和端口设置为“30502”;
  (4)选择“Ignore Unit ID”,如果不选择,测试程序返回空值。

2、基于Netty实现RTU接口

  本文Java代码,来源于modjn(github.com/klymenek/mo… 4.x实现Modbus TCP客户端和服务端(Modbus TCP client/server implementation in Java with Netty 4.x)。

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
java复制代码package de.gandev.modjn.example;

import de.gandev.modjn.ModbusClient;
import de.gandev.modjn.entity.exception.ConnectionException;
import de.gandev.modjn.entity.exception.ErrorResponseException;
import de.gandev.modjn.entity.exception.NoResponseException;
import de.gandev.modjn.entity.func.response.ReadCoilsResponse;
import de.gandev.modjn.entity.func.response.ReadHoldingRegistersResponse;

import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author XiaoYW
*
*/
public class TestModbusSlave {

public static void main(String[] args) {
ModbusClient modbusClient = ClientForTests.getInstance().getModbusClient();

ReadCoilsResponse readCoils = null;
try {
readCoils = modbusClient.readCoils(12321, 5);
} catch (NoResponseException | ErrorResponseException | ConnectionException ex) {
Logger.getLogger(Example.class.getName()).log(Level.SEVERE, ex.getLocalizedMessage());
}
System.out.println(readCoils);

ReadHoldingRegistersResponse readHolding = null;
try {
readHolding = modbusClient.readHoldingRegisters(2200,10);
} catch (NoResponseException | ErrorResponseException | ConnectionException ex) {
Logger.getLogger(Example.class.getName()).log(Level.SEVERE, ex.getLocalizedMessage());
}
System.out.println(readHolding);
modbusClient.close();
}
}

  客户端测试代码,如下文所示。

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
java复制代码package de.gandev.modjn.example;

import de.gandev.modjn.ModbusClient;
import de.gandev.modjn.entity.exception.ConnectionException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
*
* @author ares
*/
public class ClientForTests {

private final ModbusClient modbusClient;

private ClientForTests() {
modbusClient = new ModbusClient("localhost" /*
* "192.168.1.55"
*/, 30502); //ModbusConstants.MODBUS_DEFAULT_PORT);

try {
modbusClient.setup();
} catch (ConnectionException ex) {
Logger.getLogger(ClientForTests.class.getName()).log(Level.SEVERE, null, ex);
}
}

public ModbusClient getModbusClient() {
return modbusClient;
}

public static ClientForTests getInstance() {
return ClientAndServerHolder.INSTANCE;
}

private static class ClientAndServerHolder {

private static final ClientForTests INSTANCE = new ClientForTests();
}
}

这里写图片描述

  以Java应用程序运行,运行结果:

1
2
3
ini复制代码ReadCoilsResponse{byteCount=1, coilStatus={0, 3}}
ReadHoldingRegistersResponse{byteCount=20,
inputRegisters={register_0=2, register_1=0, register_2=0, register_3=3, register_4=0, register_5=0, register_6=8, register_7=0, register_8=0, register_9=0}}

参考:

《谈谈基于Netty实现Restful搭建服务架构思路》 CSDN博客 肖永威 2018年7月

《Modbus学习总结》 CSDN博客 深之JohnChen的专栏 2017年9月

本文转载自: 掘金

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

看完这篇ArrayList,工资直接+1000

发表于 2021-06-30

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

作者:JavaGieGie

微信公众号:Java开发零到壹

前言

ArrayList花Gie猜想应该所有Java的小伙伴都用过,如果还有小伙伴没用过,请文末留言,你放学给我留下来我给你补习补习。本文是集合类讲解的第一篇,选择了一个相对比较简单、大家又比较熟悉的ArrayList开篇。集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是该数据是如何存储的,集合的作用就是以一定的方式组织、存储数据。

正文

狗剩子:花Gie,新系列开启,还有点小激动呀!

毕竟同时肝几个系列,也是有点要老命的,你看我这日渐光亮的头顶,哎,啥也不说了。

狗剩子:….以后省洗发水了你,憋扯犊子了,给我说说学习集合我要注意哪几点吧!

(摸了摸光滑的头顶,若有所思)对于集合,我认为关注的点主要有以下四点:

  • 是否允许空
  • 是否允许重复数据
  • 是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致
  • 是否线程安全

狗剩子:可以说下ArrayList怎么用的吗?

首先看一下他的新增元素的方法add(),非常简单,代码如下:

1
2
3
java复制代码List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

狗剩子:看着是挺简单的,那就说下新增在底层是怎么实现的吧!

….这么突然的吗,直接肝到原理了,好吧,来来来

add方法的源码来看一下:

image.png

第2行的ensureCapacity方法是扩容用的,占时先不看。底层实际上在调用add方法的时候只是给elementData的某个位置添加了一个数据而已,用一张图表示的话是这样的:

图片

这里需要提醒一下,elementData中存储的应该是堆内存中元素的引用,而不是实际的元素,花Gie这么画图主要是为了小伙伴们理解,只要知道这个问题就好了。

狗剩子:那ensureCapacity这个扩容方法是什么原理呢?

哟呼,狗子今天当了面试官,还知道追问了。那我们先看一下,构造ArrayList的时候,默认的底层数组大小是10:

image.png

既然固定了大小,那底层数组的大小不够了怎么办?狗子都知道那就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,扩容的代码就在ensureCapacity里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 private void grow(int minCapacity) {
//1. 获取数组长度
int oldCapacity = elementData.length;

//oldCapacity >> 1 相当于除以2
//2. 新数组容量=原数组容量 * 1.5。
int newCapacity = oldCapacity + (oldCapacity >> 1);

//3. 如果新的数组容量小于传入的参数要求的最小容量minCapacity,那么新的数组容量以传入的容量参数为准。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

//4. 判断新的数组容量newCapacity是否大于数组能容纳的最大元素个数 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
//5.
newCapacity = hugeCapacity(minCapacity);

//6. 将扩容前数组放进新的扩容后的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

其中第 5 步hugeCapacity(minCapacity)用于判断传入的参数minCapacity是否大于MAX_ARRAY_SIZE,如果minCapacity大于MAX_ARRAY_SIZE,那么newCapacity等于Integer.MAX_VALUE,否者newCapacity等于MAX_ARRAY_SIZE

1
2
3
4
5
6
7
java复制代码private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

狗剩子:感觉很简单的扩容方式,那为什么要使用这种方式扩容呢?

大佬们定的,我能回答出来怎么扩容的还不够强么,还问为啥,你是十万个为什么嘛。

狗剩子:回答对了工资加五百

这样呀,那这个就好办了。

image.png

我们可以想:

1、如果一次性扩容扩得太大,必然造成内存空间的浪费

2、如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比较耗费性能的一个操作

所以扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。最后调用到的是Arrays的copyOf方法,将元素组里面的内容复制到新的数组里面去:

image.png

用一张图来表示就是这样的:

图片

狗剩子:带图示的就很棒,给你一个么么哒,除了顺序添加元素,肯定有按照下标插入的咯

你可真是个机灵鬼,ArrayList的插入操作调用的也是add方法,比如:

1
2
3
4
5
6
7
8
9
10
11
java复制代码List<String> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
list.add("666");
list.add("777");
list.add("888");
list.add(2,"000");
System.out.println(list);

有一个地方不要搞错了,第10行的add方法的意思是,往第几个下标插入数据,像第10行就是在下标为2的位置插入数据000(注意ArrayList下标从0开始,即list.get(0)的值是111)。看一下运行结果也证明了这一点:

1
csharp复制代码[111, 222, 000, 333, 444, 555, 666, 777, 888]

还是看一下插入的时候做了什么:

image.png

​ 可以看到插入的时候,先用ensureCapacity方法进行判断是否扩容按照指定位置,然后利用System.arraycopy方法从指定位置开始的所有元素做一个整体的复制,向后移动一个位置,然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。用图表示这个过程是这样的:

图片

狗剩子:哟~不错哦,那再说说删除元素吧!

ArrayList支持以下两种删除方式:

  • 按照下标删除,即list.remove(1)
  • 按照元素删除,使用方式为list.remove(“111”),如果有多个等值的元素111,也只是会删除匹配的第一个元素

从代码来看,这两种删除方法在ArrayList的实现原理差不多,都是调用的下面一段代码:

image.png

简单概括其实做的事情只有两件:

  1. 把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置
  2. 最后一个位置的元素指定为null,这样让gc可以去回收它

比方现在操作这一段代码:

1
2
3
4
5
6
7
8
9
10
java复制代码List<String> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
list.add("666");
list.add("777");
list.add("888");
list.remove("3333");

用图表示是为:

图片

狗剩子:那你总结一下ArrayList特点呗!

ArrayList应该是小白到大佬都非常常用的集合类,它是一个以数组形式实现的集合,花Gie这里用一张表格先来看一下ArrayList里面有哪些基本的元素:

元 素 作 用
private transient Object[] elementData; ArrayList是基于数组的一个实现,elementData就是底层的数组
private int size; ArrayList里面元素的个数,这里要注意一下,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1

ArrayList属性一览:

属 性 结 论
ArrayL ist是否允许空 允许
ArrayList是否允许重复数据 允许
ArrayList是否有序 有序
ArrayList是否线程安全 非线程安全

狗剩子:说了那么多,那ArrayList有啥优缺点呀

任何事物都不是完美无瑕的,我们要结合场景合理使用,ArrayList的优点如下:

  1. ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
  2. ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

不过ArrayList的缺点也十分明显:

  1. 删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
  2. 插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,简单总结就是:ArrayList比较适合顺序添加、随机访问的场景。

拓展部分

狗剩子:这里有个疑问困扰了我好久了呢,为什么ArrayList的elementData是用transient修饰的?

image.png

这个差点超出了我的认知,还好花Gie昨晚看过秘籍宝典。

image.png

我们看一下ArrayList的定义:

image.png

ArrayList实现了Serializable接口,也就是说ArrayList是可以被序列化的,而用transient修饰elementData意味着我不希望elementData数组被序列化。

这一万个草泥马奔腾而过,不能慌?因为序列化ArrayList的时候,elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

image.png

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样就会有两点好处:

  1. 加快了序列化的速度
  2. 减小了序列化之后的文件大小

总结

ArrayList的重要性大家应该都懂,这里也就不啰嗦了。我们在读源码的时候,其实有很多可以值得我们借鉴,比如elementData使用transient来修饰,学习中需要多思考,把学习到的技术和思想运用到自己实际开发中,学以致用,才能不断强大。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

+++

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

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

本文转载自: 掘金

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

王者并发课-铂金7:整齐划一-CountDownLatch如

发表于 2021-06-30

欢迎来到《王者并发课》,本文是该系列文章中的第20篇。

在上一篇文章中,我们介绍了Condition的用法。在本文中,将为你介绍CountDownLatch的用法。CountDownLatch是JUC中的一款常用工具类,当你在编写多线程代码时,如果你需要协调多个线程的开始和结束动作时,它可能会是你的不错选择。

一、CountDownLatch适用的两个典型应用场景

场景1. 协调子线程结束动作:等待所有子线程运行结束

对于资深的王者来说,下面这幅图一定再熟悉不过了。在王者开局匹配队友时,所有的玩家都必须进行确认操作,只有全部确认后才可以进入游戏,否则将进行重新匹配。如果我们把各玩家看作是子线程的话,那么就需要各子线程完成确认动作,游戏才能继续。其实,此类场景不止于王者,在生活中类似的场景还有很多。比如,所有乘客登机后飞机才能关舱门,除非超时后他们抛弃了你。

对于上图所示的玩家确认界面,如果用多线程模拟的话,那么它应该是下面的样子:

主线程创建了5个子线程,各子任务执行确认动作,期间主线程进入等待状态,直到各子线程的任务均已经完成,主线程恢复继续执行,也就是游戏继续。而如果其中某个玩家超时未执行确认的话,那么主线程将结束本次匹配,重新开始新一轮的匹配。

场景2. 协调子线程开始动作:统一各线程动作开始的时机

这个场景的例子也十分常见。比如,田径场上,各选手各就各位等待发令枪。在发令枪响之前,选手只能原地就位,否则就是违规。如果从多线程的角度看,这恰似你创建了一些多线程,但是你需要统一管理它们的任务开始时间。因为,如果你不对此做干预的话,线程调用start()之后的具体时间是不确定的,这个知识点我们早在青铜系列文章中就已经讲过。

在王者中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。总不能,你刚降生,对方已经打到你家门口了。

上述两个场景的问题,正是CountDownLatch所要解决的问题。理解了这两个问题,你也就理解了CountDownLatch存在的价值。

二、Java中的CountDownLatch设计

JUC中CountDownLatch的实现,是以类的形式存在,而不是接口,你可以直接拿过来使用。并且,它的实现还很简单。在数据结构上,CountDownLatch基于一个同步器实现,你可以看它的final Sync sync变量。

而在构造函数上,CountDownLatch有且只有CountDownLatch(int count)一个构造器,并且你需要指定数量,并且你不得在中途修改它,这点务必牢记!

核心函数

  • await():等待latch降为0;
  • boolean await(long timeout, TimeUnit unit):等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老吧。
  • countDown():latch数量减1;
  • getCount():获取当前的latch数量。

从CountDownLatch的方法上看,还是比较简单易懂的,重点要理解await()和countDown().

三、CountDownLatch如何解决场景问题

接下来,我们将第一小节的两个场景问题用代码实现一遍,让你对CountDownLatch的用法有个直观的理解。

场景1. CountDownLatch实现对各子线程的等待

创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。

在这段代码中,new CountDownLatch(5)用户创建初始的latch数量,各玩家通过countDownLatch.countDown()完成状态确认。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);

Thread 大乔 = new Thread(countDownLatch::countDown);
Thread 兰陵王 = new Thread(countDownLatch::countDown);
Thread 安其拉 = new Thread(countDownLatch::countDown);
Thread 哪吒 = new Thread(countDownLatch::countDown);
Thread 铠 = new Thread(() -> {
try {
// 稍等,上个卫生间,马上到...
Thread.sleep(1500);
countDownLatch.countDown();
} catch (InterruptedException ignored) {}
});

大乔.start();
兰陵王.start();
安其拉.start();
哪吒.start();
铠.start();
countDownLatch.await();
System.out.println("所有玩家已经就位!");
}

场景2. CountDownLatch实现对多线程的统一管理

在这个场景中,我们仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各玩家虽然都调用了start()线程,但是它们在运行时都在等待countDownLatch的信号,在信号未收到前,它们不会往下执行。

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
java复制代码public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);

Thread 大乔 = new Thread(() -> waitToFight(countDownLatch));
Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch));
Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
Thread 铠 = new Thread(() -> waitToFight(countDownLatch));

大乔.start();
兰陵王.start();
安其拉.start();
哪吒.start();
铠.start();
Thread.sleep(1000);
countDownLatch.countDown();
System.out.println("敌方还有5秒达到战场,全军出击!");
}

private static void waitToFight(CountDownLatch countDownLatch) {
try {
countDownLatch.await(); // 在此等待信号再继续
System.out.println("收到,发起进攻!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

运行结果如下,各玩家在收到出击信号发起了进攻:

1
2
3
4
5
6
7
8
shell复制代码敌方还有5秒达到战场,全军出击!
收到,发起进攻!
收到,发起进攻!
收到,发起进攻!
收到,发起进攻!
收到,发起进攻!

Process finished with exit code 0

小结

以上就是关于CountDownLatch的全部内容。总体上,CountDownLatch比较简单且易于理解。在学习时,先了解其设计意图,再写个Demo基本就能流畅掌握。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

  • 编写代码体验CountDownLatch用法。

延伸阅读与参考资料

  • 《并发王者课》大纲与更新进度总览

最新修订及更好阅读体验

  • 阅读掘金原文

关于作者

从业近十年,先后从事敏捷与DevOps咨询、Tech Leader和管理等工作,对分布式高并发架构有丰富的实战经验。热衷于技术分享和特定领域书籍翻译,掘金小册《高并发秒杀的设计精要与实现》作者。


关注公众号【MetaThoughts】,及时获取文章更新和文稿。

如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

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

APP调起支付返回:-1

发表于 2021-06-30

错误信息

UNIAPP错误提示信息如下

1
2
3
json复制代码  "errMsg": "requestPayment:fail [payment微信:-1]General errors",
"errCode": -100,
"code": -100

由错误信息可以看到微信返回的错误码是-1,查询微信的官方文档得知可能的原因:

  1. 签名错误。
  2. 未注册APPID。
  3. 项目设置APPID不正确。
  4. 注册的APPID与设置的不匹配。
  5. 其他异常等。

排查思路

  1. 检查menifest.json中配置的微信appid是否和微信开放平台应用的appid、服务端使用的appid一致。
  2. android版云打包,数字签名,与微信开放平台配置一致,开放平台填写的应用签名要去掉冒号,要小写。改完应用签名要过一段时间才生效。
  3. 服务端的返回数据格式,注意字段名字或大小写。
1
2
3
4
5
6
7
8
9
json复制代码{
"appid": "",
"partnerid": "", // 商户ID
"prepayid": "", // 预支付id
"noncestr": "", // 这个参数有些资料说要和唤起支付接口时使用的值一致,经测试无需一致也可以
"timestamp": ,
"package": "Sign=WXPay", // 固定值
"sign": "" // 这个签名是 用上面的参数二次签名得到的,和下单接口的签名不一致
}
  1. “唤起支付接口”中的签名类型是否与“统一下单接口”的类型一致,密钥为32位时,签名一般是MD5。
  2. 微信退出重启一下。这里先尝试了清除缓存,测试无效后,退出账号重新登录后可以了。

问题

1、申请微信开放平台时,应用签名和应用包名都填写成了包名,根据排查2步处理解决。
image.png
2、签名修改正确后,测试还是不行,一直在排查各种可疑问题,最后还是微信退出账号重新登录解决了

参考

微信开放社区知识库

本文转载自: 掘金

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

Java性感程序员花2天实现基于java ssm sprin

发表于 2021-06-30

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

夏天到了、小雪来给大家降降温

话不多说、直接进入主题

目录​

主要功能模块设计:

主要技术:

主要功能实现前端:

选课平台首页:

登录注册管理:

选课推荐分类:

我的个人中心:

主要功能实现前端:

管理员登录:

选课类型管理:

选课信息详情管理:

管理员信息管理:

用户信息管理:

评论交流回复管理:

部分关键代码展示:

登录模块:

配置模块:

主要表设计:

用户表:

选课类型表:

选课详情表:

评论交流表:

回复信息表:

主要功能模块设计:

登录注册、首页信息浏览、选课分类查看、选课详情查看、评论交流、收藏、浏览量、以及后台数据管理、用户管理、选课类别管理、选课信息详情管理、评论交流和回复管理、以及公告信息管理等

主要技术:

Java、springmvc、mybatis、mysql、tomcat、jquery、layui、JavaScript、html、css、jsp、log4j等一些常见的基本技术。

主要功能实现前端:

选课平台首页:

输入http://localhost/访问选课推荐交流平台首页、可以查看轮播图以及各类信息、点击进入详情页面

登录注册管理:

选课推荐分类:

点击查看分类课程推荐信息、按照类别查看课程信息、管理员可以在后台添加课程分类信息

以及根据访问量来进行点击排行、

课程详情信息:

课程详情可以查看课程详情信息以及作者信息和浏览量等具体数据、也可以进行评论和收藏等操作

我的个人中心:

包括我的个人信息和收藏夹信息、

主要功能实现后台:

系统主页设计:

主要功能模块有首页的信息统计、选课类型管理、选课详情管理、用户管理、评论和公告管理等数据维护。

选课类型管理:

选课信息详情管理:

列表信息查看添加修改删除以及检索等操作

详情信息

通知公告信息:

数据列表查看和添加修改删除等操作

用户信息管理:

评论交流回复管理:

评论和回复的数据管理

部分关键代码展示:

登录模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
ini复制代码package com.longwang.controller;

import com.longwang.entity.Article;
import com.longwang.entity.Classify;
import com.longwang.entity.User;
import com.longwang.service.ArticleService;
import com.longwang.service.ClassifyService;
import com.longwang.service.NoticeService;
import com.longwang.service.UserService;
import com.longwang.util.DateUtil;
import com.longwang.util.StringUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.util.*;

/**
* 根路径以及其他请求处理
*
* @author 李杨勇
*
*/
@Controller
public class IndexController {

@Value("${imageFilePath}")
private String imageFilePath; // 文件路径

@Resource
private NoticeService noticeService;

@Resource
private UserService userService;

@Resource
private ArticleService articleService;
@Resource
private ClassifyService classifyService;



@RequestMapping("/")
public String index(HttpSession session) {

// 查询公告
session.setAttribute("noticeList", noticeService.list(0, 5));
return "index";// 跳转到index.html
}


@RequestMapping("/delete")
public Map<String, Object> delete(Integer userId) {
Map<String, Object> resultMap = new HashMap<String, Object>();
userService.delete(userId);
resultMap.put("errorNo", 0);
return resultMap;
}
/**
* 登录页面
*
* @return
*/
@RequestMapping("/login")
public String login() {
return "login";
}

/**
* 前台登录页面
*
* @return
*/
@RequestMapping("/webLogin")
public String webLogin() {
return "webLogin";
}

/**
* 注册
*
* @return
*/
@RequestMapping("/regist")
public String regist() {
return "regist";
}

/**
* 保存注册信息
*
* @param user
* @return
*/
@RequestMapping("/saveUser")
public String saveUser(User user) {
List<Article> randomArticle = articleService.getRandomArticle(3);
String ids="";
for (int i = 0; i < randomArticle.size(); i++) {
Integer articleId = randomArticle.get(i).getArticleId();
ids+=articleId+",";
}
ids = ids.substring(0, ids.length() -1);
user.setArticleIds(ids);
userService.save(user);

return "webLogin";
}

/**
* 退出登录
*
* @param request
* @return
*/
@RequestMapping("/quit")
public String quit(HttpServletRequest request) {
HttpSession session = request.getSession();
session.removeAttribute("user");
return "index";
}


/**
* 退出登录
*
* @param request
* @return
*/
@RequestMapping("/quitAdmin")
public String quitAdmin(HttpServletRequest request) {
HttpSession session = request.getSession();
session.removeAttribute("user");
return "login";
}

/**
* 验证登录
*
* @param user
* @param request
* @return
*/
@RequestMapping("/checkLogin")
public ModelAndView checkLogin(User user, HttpServletRequest request) {
ModelAndView mav = new ModelAndView();
HttpSession session = request.getSession();
User u = userService.findByUsernameAndPassword(user.getUsername(), user.getPassword());
if (u == null) {
mav.addObject("user", user);
mav.addObject("errorInfo", "用户名或者密码错误!");
mav.setViewName("webLogin");
} else {
u.setLatelyLoginTime(new Date());
userService.save(u);
session.setAttribute("user", u);
mav.addObject("username", u.getUsername());
mav.addObject("user", u);
mav.addObject("success", true);
mav.setViewName("/index");
}
return mav;
}

/**
* 查看个人信息
*
* @return
*/
@RequestMapping("viewPerson")
public ModelAndView viewPerson(HttpServletRequest request) {
User user = (User) request.getSession().getAttribute("user");
ModelAndView mav = new ModelAndView();
User u = userService.findById(user.getUserId());
mav.addObject("user", u);
mav.setViewName("/viewPerson");
return mav;
}

/**
* 查看个人课程收藏夹
*
* @return
*/
@RequestMapping("viewCollection")
public ModelAndView viewCollection(HttpServletRequest request, HttpSession session) {
User user = (User) request.getSession().getAttribute("user");
ModelAndView mav = new ModelAndView();
User u = userService.findById(user.getUserId());
String artIds = u.getArticleIds();
List<String> result = new ArrayList<>();
if (StringUtils.isNotBlank(artIds)) {
result = Arrays.asList(StringUtils.split(artIds, ","));
}
List<Integer> retIds = new ArrayList<>();
for (String temp : result) {
retIds.add(Integer.valueOf(temp).intValue());
}
List<Article> retArt = articleService.findByListId(retIds);
session.setAttribute("noticeList", noticeService.list(0, 5));
mav.addObject("retArt", retArt);
mav.addObject("user", u);
mav.setViewName("/viewCollection");
return mav;
}

/**
* 查看个人关注用户
*
* @return
*/
@RequestMapping("viewFocusUser")
public ModelAndView viewFocusUser(HttpServletRequest request, HttpSession session) {
User user = (User) request.getSession().getAttribute("user");
ModelAndView mav = new ModelAndView();
User u = userService.findById(user.getUserId());
String userIds = u.getUserIds();
List<String> result = new ArrayList<>();
if (StringUtils.isNotBlank(userIds)) {
result = Arrays.asList(StringUtils.split(userIds, ","));
}
List<Integer> retIds = new ArrayList<>();
for (String temp : result) {
retIds.add(Integer.valueOf(temp).intValue());
}
List<User> retArt = userService.findByListId(retIds);
session.setAttribute("noticeList", noticeService.list(0, 5));
mav.addObject("retArt", retArt);
mav.addObject("user", u);
mav.setViewName("/viewFocusUser");
return mav;
}

/**
* 保存用户信息
*
* @param user
* @return
*/
@RequestMapping("/save")
public ModelAndView save(User user) {
ModelAndView mav = new ModelAndView();
userService.save(user);
mav.setViewName("/index");
return mav;
}

/**
* 写笔记页面
*
* @param request
* @return
*/
// @RequestMapping("notePage")
// public String notePage(HttpServletRequest request, Model model) {
// User user = (User) request.getSession().getAttribute("user");
// if (user == null) {
// return "webLogin";
// }
// List<Classify> list = classifyService.findAll();
// model.addAttribute("list", list);
// return "one";
// }

@RequestMapping("notePage")
public ModelAndView notePage(HttpServletRequest request) {
ModelAndView mav = new ModelAndView();
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
mav.setViewName("/webLogin");
return mav;
}
List<Classify> list = classifyService.findAll();
mav.addObject("list", list);
mav.setViewName("/one");
return mav;
}

/**
* 保存笔记
*
* @param article
* @param request
* @return
*/
@RequestMapping("addNote")
public ModelAndView addNote(Article article, HttpServletRequest request) {
ModelAndView mav = new ModelAndView();
// 获取当前用户信息
User user = (User) request.getSession().getAttribute("user");
article.setUserId(user.getUserId());
article.setPublishDate(new Date());
article.setClick(0);
article.setCommentNum(0);
article.setContentNoTag(StringUtil.Html2Text(article.getContent()));
articleService.save(article);
mav.setViewName("/index");
return mav;
}

@RequestMapping("saveNote")
public ModelAndView saveNote(Article article, HttpServletRequest request) {
ModelAndView mav = new ModelAndView();
Article a = articleService.findById(article.getArticleId());
article.setPublishDate(a.getPublishDate());
// 获取当前用户信息
articleService.save(article);
mav.setViewName("/index");
return mav;
}

/**
* 查看笔记
*
* @return
*/
@RequestMapping("viewNote")
public String viewNote(HttpSession session) {
session.setAttribute("noticeList", noticeService.list(0, 5));
return "mylist";
}

@RequestMapping("/delete/{id}")
public String delete(@PathVariable(value = "id") String id) throws Exception {
articleService.delete(Integer.parseInt(id));
return "mylist";
}

/**
* 查看个人笔记加载数据列表
*
* @param article
* @param publishDates
* @param page
* @param pageSize
* @return
*/
@RequestMapping("/mylist")
public Map<String, Object> list(Article article,
@RequestParam(value = "publishDates", required = false) String publishDates,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize, HttpServletRequest request) {
Map<String, Object> resultMap = new HashMap<String, Object>();
// User user = (User) request.getSession().getAttribute("user");
// article.setUserId(user.getUserId());
String s_bPublishDate = null; // 开始时间
String s_ePublishDate = null; // 结束时间
if (StringUtil.isNotEmpty(publishDates)) {
String[] strs = publishDates.split(" - "); // 拆分时间段
s_bPublishDate = strs[0];
s_ePublishDate = strs[1];
}
Long total = articleService.getCount(article, s_bPublishDate, s_ePublishDate);
int totalPage = (int) (total % pageSize == 0 ? total / pageSize : total / pageSize + 1); // 总页数
resultMap.put("totalPage", totalPage);
resultMap.put("errorNo", 0);
resultMap.put("data", articleService.list(article, s_bPublishDate, s_ePublishDate, page - 1, pageSize));
resultMap.put("total", total);
return resultMap;
}

/**
* 后台默认首页
*
* @return
*/
@RequestMapping("/index")
public String root() {
return "/common/index";
}

/**
* 博主信息页面
*
* @return
*/
@RequestMapping("/blogger")
public String blogger() {
return "/blogger/index";
}

/**
* 图片上传处理 @Title: ckeditorUpload @param file 图片文件 @return 参数说明 @return
* Map<String,Object> 返回类型 @throws
*/
@ResponseBody
@RequestMapping("/upload")
public Map<String, Object> ckeditorUpload(@RequestParam("file") MultipartFile file) {
Map<String, Object> resultMap = new HashMap<String, Object>();
Map<String, Object> resultMap1 = new HashMap<String, Object>();
String fileName = file.getOriginalFilename(); // 获取文件名
String suffixName = fileName.substring(fileName.lastIndexOf(".")); // 获取文件的后缀
String newFileName = "";
try {
newFileName = DateUtil.getCurrentDateStr() + suffixName; // 新文件名
FileUtils.copyInputStreamToFile(file.getInputStream(), new File(imageFilePath + newFileName)); // 上传
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
resultMap.put("code", 0);
resultMap1.put("filePath", newFileName);
resultMap.put("data", resultMap1);
return resultMap;
}

}

配置模块:

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
yaml复制代码server: 
port: 80
servlet:
context-path: /

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/choosing_courses?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQLDialect
thymeleaf:
cache: false

thymeleaf:
prefix: classpath:/templates/

MD5Salt: longwang


imageFilePath: C:\\Users\\Administrator\\Desktop\\choosingCourses\\src\\main\\webapp\\static\\images\\
downloadImagePath: C:\\Users\\Administrator\\Desktop\\choosingCourses\\src\\main\\webapp
lucenePath: C:\\Users\\Administrator\\Desktop\\choosingCourses\\lucene

主要表设计:

用户表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码CREATE TABLE `NewTable` (
`user_id` int(11) NOT NULL AUTO_INCREMENT ,
`head_portrait` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`lately_login_time` datetime NULL DEFAULT NULL ,
`nickname` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`registration_date` datetime NULL DEFAULT NULL ,
`sex` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`open_id` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`article_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`birthday` date NULL DEFAULT NULL ,
`momo` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`user_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`user_id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=20
ROW_FORMAT=COMPACT
;

选课类型表:

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `NewTable` (
`classify_id` int(11) NOT NULL AUTO_INCREMENT ,
`classify_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL ,
PRIMARY KEY (`classify_id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=27
ROW_FORMAT=COMPACT
;

选课详情表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码CREATE TABLE `NewTable` (
`article_id` int(11) NOT NULL AUTO_INCREMENT ,
`author` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL ,
`click` int(11) NULL DEFAULT NULL ,
`comment_num` int(11) NULL DEFAULT NULL ,
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL ,
`image_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL ,
`is_original` int(11) NULL DEFAULT NULL ,
`is_top` int(11) NULL DEFAULT NULL ,
`publish_date` datetime NULL DEFAULT NULL ,
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL ,
`classify_id` int(11) NULL DEFAULT NULL ,
`user_id` int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`article_id`),
FOREIGN KEY (`classify_id`) REFERENCES `t_classify` (`classify_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
INDEX `FKo4fros4yfq1m9ay7sgtlcvbc4` (`classify_id`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=58
ROW_FORMAT=COMPACT
;

评论交流表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码CREATE TABLE `NewTable` (
`comment_id` int(11) NOT NULL AUTO_INCREMENT ,
`comment_date` datetime NULL DEFAULT NULL ,
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL ,
`article_id` int(11) NULL DEFAULT NULL ,
`user_id` int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`comment_id`),
FOREIGN KEY (`article_id`) REFERENCES `t_article` (`article_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
FOREIGN KEY (`user_id`) REFERENCES `t_user` (`user_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
INDEX `FKlsvvc2ob8lxg2m9qqry15ru0y` (`article_id`) USING BTREE ,
INDEX `FKtamaoacctq4qpko6bvtv0ke1p` (`user_id`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=15
ROW_FORMAT=COMPACT
;

回复信息表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码CREATE TABLE `NewTable` (
`reply_id` int(11) NOT NULL AUTO_INCREMENT ,
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL ,
`reply_date` datetime NULL DEFAULT NULL ,
`comment_id` int(11) NULL DEFAULT NULL ,
`user_id` int(11) NULL DEFAULT NULL ,
PRIMARY KEY (`reply_id`),
FOREIGN KEY (`comment_id`) REFERENCES `t_comment` (`comment_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
FOREIGN KEY (`user_id`) REFERENCES `t_user` (`user_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
INDEX `FKk4ydp71wampdbnguly8iks4rf` (`comment_id`) USING BTREE ,
INDEX `FKslt6r79iw1p9cbxns09erjv6v` (`user_id`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=6
ROW_FORMAT=COMPACT
;

本文转载自: 掘金

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

1…625626627…956

开发者博客

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