目录
一、发布问题---控制器层
当前用户登录是在straw-gateway
的服务器上处理的,当登录成功后,表示该用户的信息的Session数据将保存在straw-gateway
的服务器的内存,而其它服务器(例如straw-api-question
)需要读取Session中的数据以识别用户的身份,及读取相关信息,但是,其它服务器不可能访问straw-gateway
服务器的内存!为了实现共享Session,可以在登录成功时,将用户的Session数据保存到Redis服务器中,而不再是保存在内存中,当其它服务器需要获取Session数据时,从Redis服务器中直接获取即可!
为了实现这个目标,需要先添加相关依赖,参考代码:
<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
先在父级项目中管理以上依赖,然后,在straw-gateway
中添加该依赖,同时,在straw-gateway
中还需要添加spring-boot-starter-data-redis
依赖:
在straw-gateway
的application.properties
中配置“使用Redis保存Session数据”:
最后,在StrawGatewayApplication
类的声明之前添加@EnableRedisHttpSession
注解(也可以不添加):
完成后,启动straw-gateway
项目(其它的项目可以暂时不启动,在控制台会提示一些错误信息,可以无视),在浏览器打开登录页面,输入正确的登录信息,登录成功后,在终端通过redis-cli
登录Redis客户端控制台,输入keys *
即可看到在Redis存在Session数据:
已经能够将Session保存到Redis中之后,在straw-api-question
中就需要读取Session中的信息,由于Session信息是Spring Security框架组织的,存入的是LoginUserInfo
类型的对象,则取出来也是LoginUserInfo
类型的,则straw-api-question
项目必须添加Spring Security的依赖,否则,将无法识别,并且,也添加spring-session-data-redis
依赖:
由于添加了Spring Security依赖后,当前项目默认就会要求所有请求都是需要登录的!但是,在当前集群中,应该使用网关统一处理登录相关的验证及授权,其它各微服务项目是不需要验证登录的,所以,在straw-api-question
项目添加配置,对所有请求直接许可,不再验证用户的身份!这个问题在此前开发straw-api-user
时也出现过,则直接将straw-api-user
中的WebSecurityConfigurer
配置类复制到straw-api-question
的cn.tedu.straw.api.question.security
包中即可:
并且,在StrawApiQuestionApplication
中添加@EnableWebSecurity
注解:
完成后,已经可以实现straw-gateway
写入Session,由其它服务器读取Session的操作了!但是,除了登录以外的各功能都不是写在straw-gateway
项目中的,是客户端直接访问straw-gateway
网关,然后由straw-gateway
网关再将请求转发到其它服务器,由其它服务器负责具体的处理!
在默认情况下,straw-gateway
中使用的Zuul网关在转发时,会将请求(Request)中的请求头(Request Headers)中的Cookie和Set-Cookie视为敏感信息,在转发时不会转发这2项数据,就会导致后面的服务器(例如straw-api-question
运行所在的服务器)收到的请求中,请求头不会包含Cookie和Set-Cookie的数据,就无法读取Session(无论Session在哪里都读不到)!为了解决这个问题,需要在straw-gateway
的application.properties
中添加配置,将Cookie和Set-Cookie设置为“不敏感的”,则在转发时就会携带请求头中的Cookie和Set-Cookie,以至于后续的其它服务器可以访问到Session数据:
以上配置的
sensitive-headers
属性的作用就是“配置敏感请求头”,在等于号的右侧没有写任何值,则表示“任何请求头中的数据都是不敏感的”。
完成后,暂时无法验证以上操作是否成功,应该尝试从控制器中读取Session中保存的数据!
首先,在straw-api-question
项目中添加Spring Validation的依赖,以应用Spring Validation的验证机制。
应该在PostQuestionDTO
类的各属性之前添加Spring Validation的相关注解,用于约定客户端提交的请求参数的基本格式,例如:
package cn.tedu.straw.api.question.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class PostQuestionDTO implements Serializable {
@NotNull(message = "发布问题失败!请填写问题的标题!")
@Size(min = 2, max = 100, message = "发布问题失败!标题的长度必须是2~100个字符之间!")
private String title;
private String content;
private Integer[] tagIds;
private Integer[] teacherIds;
}
关于数据的格式的验证规则,应该自行决定,不同的项目、不同的功能,验证规则可能都不相同。
当需要获取当前登录的用户信息时,在控制器中,在处理请求的方法的参数列表中,通过@AuthenticationPricipal
注解装配LoginUserInfo
类型的参数,在处理请求过程中,通过该参数对象即可获取相关信息。
在QuestionController
中进行配置,以处理“发布问题”的请求:
package cn.tedu.straw.api.question.controller;
import cn.tedu.straw.api.question.dto.PostQuestionDTO;
import cn.tedu.straw.api.question.service.IQuestionService;
import cn.tedu.straw.commons.ex.IllegalParameterException;
import cn.tedu.straw.commons.security.LoginUserInfo;
import cn.tedu.straw.commons.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* <p>
* 前端控制器
* </p>
*
* @author tedu.cn
* @since 2020-09-16
*/
@RestController
@RequestMapping("/v1/questions")
@Slf4j
public class QuestionController {
@Autowired
IQuestionService questionService;
// http://localhost:8081/v1/questions/post?title=TestTitle&content=TestContent&tagIds=2&tagIds=5&teacherIds=1&teacherIds=3&teacherIds=5
// http://localhost/api-question/v1/questions/post?title=TestTitle2&content=TestContent2&tagIds=2&tagIds=5&teacherIds=1&teacherIds=3&teacherIds=5
@RequestMapping("/post")
public R post(@Valid PostQuestionDTO postQuestionDTO,
BindingResult bindingResult,
@AuthenticationPrincipal LoginUserInfo loginUserInfo) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldError().getDefaultMessage();
throw new IllegalParameterException(errorMessage);
}
log.debug("从Session中获取当前登录的用户信息:{}", loginUserInfo);
Integer userId = loginUserInfo.getId();
String userNickName = loginUserInfo.getNickname();
log.debug(">>> Session中的用户id={}", userId);
log.debug(">>> Session中的用户昵称={}", userNickName);
questionService.postQuestion(postQuestionDTO, userId, userNickName);
return R.ok();
}
}
完成后,先重启straw-eureka-server
,再重启2个api项目,最后重启straw-gateway
,在浏览器中,通过例如 http://localhost/api-question/v1/questions/post?title=TestTitle2&content=TestContent2&tagIds=2&tagIds=5&teacherIds=1&teacherIds=3&teacherIds=5 这样的路径即可发布问题(首次访问会先重定向到登录页,登录成功后即可发布成功)。
二、发布问题---前端页面
1.使用模拟数据显示下拉菜单中的选项
在页面中的选择“问题标签”的下拉列表是通过Vue-Select插件制作的效果:
原本的HTML部分的代码是:
可以看到,目前的模拟数据是直接写在HTML代码中的,不便于调整,当需要通过Vue来管理这个列表中的选项时,应该先确定当前“发布问题”的表单区域范围,为这个范围的父级标签添加id
属性,用于创建Vue对象!
当添加id
后,就可以基于这个id
对应的标签来创建Vue对象了!在static/js
下创建question/create.js
文件:
然后,在create.js
中就可以创建Vue对象了:
在create.html
中引用该文件:
接下来,需要调整HTML部分的下拉菜单的语法,改为:
在HTML中,可以在控件(例如输入框、按钮、下拉菜单、单选按钮、复选框等)上配置
v-model
属性,该属性是用于绑定控件的value
属性的!
并且,在create.js
中声明对应的属性,并为属性赋予测试数据作为值:
以上测试数据中,
label
和value
这2个名称是固定的,是Vue-Select处理下拉列表选项时默认使用的“列表项显示的文字”和“选中该选项后将提交的id”的名称。
完成后,重新启动straw-gateway
项目(其它相关项目应该也是启动状态),刷新“发布问题”页面,即可看到配置的模拟数据,并且最多只能选择3项。
使用同样的做法,使用模拟数据显示老师的列表:
完成后,再次重启项目检查运行效果。
2.使用真实数据显示“问题标签列表”下拉菜单
先向服务器端发送请求,尝试获取“问题标签列表”,然后,将服务器响应的“问题标签列表”赋值给Vue的tags
属性即可!
3.使用真实数据显示“老师列表”下拉菜单
目前,并没有控制器能处理“获取老师列表”的请求,所以,需要先实现该功能!
先在数据库中直接将某些用户数据标识为“老师”:
然后,遵循“持久层 -> 业务层 -> 控制器层”的开发顺序,开发“获取老师列表”的功能!
查询老师列表时,需要执行的SQL语句大致是:
select id, nickname from user where account_type=1 order by id
目前,并没有哪个数据类型适合封装此次的查询结果(如果使用User
类是可以封装的,但是,User
类中的属性太多,用不上),则在straw-commons
的cn.tedu.straw.commons.vo
包中创建TeacherSelectOptionVO
类:
@Data
public class TeacherSelectOptionVO implements Serializable {
private Integer id;
private String nickname;
}
在straw-api-user
项目中,在cn.tedu.straw.api.user.mapper
包的UserMapper
接口中添加抽象方法:
/**
* 查询老师的列表,用于显示下拉菜单
*
* @return 老师的列表
*/
List<TeacherSelectOptionVO> findTeachers();
然后,在UserMapper.xml
中配置以上抽象方法的映射:
<select id="findTeachers" resultType="cn.tedu.straw.commons.vo.TeacherSelectOptionVO">
select id, nickname from user where account_type=1 order by id
</select>
完成后,应该“写一层,测一层”,在test
的cn.tedu.straw.api.user.UserMapperTests
中编写并执行单元测试:
@Test
void findTeachers() {
List<TeacherSelectOptionVO> teachers = mapper.findTeachers();
System.err.println("老师列表长度:" + teachers.size());
for (TeacherSelectOptionVO teacher : teachers) {
System.err.println(">>> " + teacher);
}
}
当测试通过后,继续向后完成业务层的开发,先在IUserService
接口中声明抽象方法:
/**
* 查询老师的列表,用于显示下拉菜单
*
* @return 老师的列表
*/
List<TeacherSelectOptionVO> getTeacherList();
然后,在UserServiceImpl
中实现以上抽象方法:
@Override
public List<TeacherSelectOptionVO> getTeacherList() {
return userMapper.findTeachers();
}
完成后,在test
的cn.tedu.straw.api.user.service.UserServiceTests
中编写并执行单元测试:
@Test
void getTeacherList() {
List<TeacherSelectOptionVO> teachers = service.getTeacherList();
System.err.println("老师列表长度:" + teachers.size());
for (TeacherSelectOptionVO teacher : teachers) {
System.err.println(">>> " + teacher);
}
}
当测试通过后,继续向后开发控制器层的功能,在UserController
中添加处理请求的方法:
// http://localhost:8080/v1/users/teachers/select-option
// http://localhost/api-user/v1/users/teachers/select-option
@GetMapping("/teachers/select-option")
public R<List<TeacherSelectOptionVO>> getTeacherList() {
return R.ok(userService.getTeacherList());
}
完成后,重启straw-api-user
项目,通过 http://localhost:8080/v1/users/teachers/select-option 测试访问,当访问成功后,确保straw-eureka-server
、straw-gateway
已经启动,再通过 http://localhost/api-user/v1/users/teachers/select-option 测试访问。
当测试通过后,在static/js/question/create.js
中,补充声明loadTeachers
函数,在created
对应的函数中调用它:
并且loadTeachers
函数加载服务器端响应的老师列表,并显示在界面中:
完成后,重启straw-gateway
,在“发布问题”页面的表单中,可以看到正确的“老师列表”。
4.通过前端页面发布问题
首先,需要为发布问题的<form>
表单绑定提交事件:
并在Vue的方法列表中声明对应的方法:
接下来,就需要在postQuestion()
方法中获取表单的相关信息,2个下拉列表的值已经通过此前准备的selectedTagIds
、selectedTeacherIds
来表示,剩下的还有“标题”和“正文”需要获取,则先为“标题”的输入框绑定Vue属性:
在Vue中也声明该属性:
至于“正文”的值,不能通过以上方式直接绑定,由于在页面中,输入正文的文本域已经配置了id
,则需要获取正文的值时,通过jQuery相关语法即可获得!
在postQuestion()
方法,测试输出页面中填写和选择的值:
完成后,重启straw-gateway
,刷新页面,输入和选择数据后,打开浏览器的控制台,点击“发布问题”的按钮,观察控制台的输出结果是否正确。
其实,在使用Vue-Select时,当选中了某个选项后,所配置例如selectedTagIds
数组中,存入的并不是选中的选项的id
,而是整个选项对象,也就是例如{ label: 'Java基础', value: 1}
,所以,需要在HTML中的标签上使用:reduce
进行配置:
注意:页面中的2个下拉列表都需要补充这条属性!
然后,在create.js
中完成提交即可:
由于
$.ajax()
在提交数组类型的数据时,将提交为tagIds: [1,2,3]
这样的格式,SpringMVC是不支持的,所以,在$.ajax()
函数中需要补充traditional: true
,则$.ajax()
会将数组处理为tagIds=1&tagIds=2&tagIds=3
这种SpringMVC可以支持的格式。