从单体项目到微服务
上次课网关项目的路由原理
单体服务器拆分为微服务
主要拆分的目标就是straw-portal
拆分后大概由下列项目组成
- straw-sys:系统基础服务,用户管理等
- straw-faq:问答系统,负责问答系统核心功能
- straw-resource:静态资源服务,图片的上传下载
- straw-search:问题搜索
- straw-gateway:网关,UI界面和系统安全
项目拆分以后真实环境下部署运行时,一定会部署到不同的服务器上
整个项目的计算和业务,又多台服务器共同承担,是软件计算性能大大提升
这种部署模式就是"分布式系统"
将UI界面迁移到网关
启动straw-eureka 然后启动straw-gateway:
输入http://localhost:9000/upload.html 能显示页面即可
然后依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
创建HomeController
编写代码如下
@RestController
@Slf4j
public class HomeController {
@GetMapping("/register.html")
public ModelAndView register(){
return new ModelAndView("register");
}
}
重启服务
输入路径:http://localhost:9000/register.html
能显示注册页面即可
学生注册功能的迁移
首先创建系统基本服务模块的微服务
straw-sys
子项目pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>straw</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>straw-sys</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-sys</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-starter-netflix-eureka-client
</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
父项目pom.xml加modules
<modules>
<!-- 省略其它模块配置 ... -->
<module>straw-sys</module>
</modules>
straw-sys的application.properties
server.port=8002
spring.application.name=sys-service
主程序类注解
@SpringBootApplication
@EnableEurekaClient
public class StrawSysApplication {
public static void main(String[] args) {
SpringApplication.run(StrawSysApplication.class, args);
}
}
启动Sys模块,检查eureka是否注册成功
gateway模块设置路由 添加sys模块的路径
zuul.routes.sys.path=/sys/**
zuul.routes.sys.service-id=sys-service
再回到sys模块中添加一个控制器测试访问效果
@RestController
@RequestMapping("/v1/sys")
public class DemoController {
@GetMapping("/demo")
public String demo(){
return "Hello sys!!!";
}
}
重启服务gateway,sys
测试输入路径:http://localhost:9000/sys/v1/sys/demo
页面上出现Hello sys!!!即可
开始编写通用代码模块
创建straw-commons模块
这个模块保存所有项目都需要的通用代码,例如实体类
创建完毕之后先父子相认
子项目pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>straw</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>straw-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-commons</name>
<description>Demo project for Spring Boot</description>
</project>
父项目中添加modules
<modules>
<!-- 其它的模块 ... -->
<module>straw-commons</module>
</modules>
复制过程如图
commons模块中实体类需要Mybatis的依赖所有pom.xml文件添加如下代码
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>
开始迁移注册功能
**转回到sys模块!!! **
首先sys模块需要commons的组件,pom文件中添加
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>straw-commons</artifactId>
</dependency>
将注册功能迁移到sys模块的几个部分
- 数据访问层
- 业务逻辑层
- 控制器
- 配置网关路由
迁移数据层
复制完毕之后
sys项目的pom.xml文件添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
sys的主方法类中修改代码如下
@SpringBootApplication
@EnableEurekaClient
@MapperScan("cn.tedu.straw.sys.mapper")
public class StrawSysApplication {
public static void main(String[] args) {
SpringApplication.run(StrawSysApplication.class, args);
}
}
sys的application.properties文件也要配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/straw?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=root
logging.level.cn.tedu.straw.portal.mapper=trace
logging.level.cn.tedu.straw.portal=debug
测试拆分后的数据访问层是否能够正常执行
@Resource
ClassroomMapper classroomMapper;
@Resource
UserMapper userMapper;
@Resource
UserRoleMapper userRoleMapper;
@Test
void contextLoads() {
System.out.println(classroomMapper);
System.out.println(userMapper);
System.out.println(userRoleMapper);
}
迁移业务逻辑层代码
需要将一些不必要的方法删除!
IUserService代码修改如下
public interface IUserService extends IService<User> {
//用户注册的方法(现在是针对学生注册)
void registerStudent(RegisterVo registerVo);
//查询所有老师用户的方法
List<User> getMasters();
//查询所有老师用户的Map方法
Map<String,User> getMasterMap();
//查询当前登录用户信息面板的方法
//这个方法的参数有变化!!!!注意!!!!
UserVo currentUserVo(String username);
}
UserServiceImpl实现类也要随之修改
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired(required = false)
UserMapper userMapper;
@Autowired
ClassroomMapper classroomMapper;
@Autowired
UserRoleMapper userRoleMapper;
BCryptPasswordEncoder passwordEncoder=
new BCryptPasswordEncoder();
@Override
@Transactional
public void registerStudent(RegisterVo registerVo) {
//判断registerVo非空
if(registerVo==null){
//如果信息是空则发生异常
//这里的异常逻辑是我们编写的项目发生的,不是系统异常
//所以这里以及以后的方法中都需要抛出自定义的异常
throw ServiceException.unprocesabelEntity("表单数据为空");
}
//根据输入的邀请码查询班级,验证邀请码有效性
QueryWrapper<Classroom> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("invite_code",registerVo.getInviteCode());
Classroom classroom=classroomMapper.selectOne(queryWrapper);
log.debug("邀请码对应的班级为:{}",classroom);
if(classroom==null){
throw ServiceException.unprocesabelEntity("邀请码错误!");
}
//验证数据库中是否已经注册过输入的用户名(手机号)
//用户名查询用户对象
User u=userMapper.findUserByUsername(registerVo.getPhone());
if(u!=null){
//用户已存在
throw ServiceException.unprocesabelEntity("手机号已经注册!");
}
//User对象的赋值(将表单中的值和一些默认值确定后)
User user=new User();
user.setUsername(registerVo.getPhone());
user.setPhone(registerVo.getPhone());
user.setNickname(registerVo.getNickname());
//用户输入的是明文密码,数据库保存的是带算法ID的加密结果!
user.setPassword("{bcrypt}"+
passwordEncoder.encode(registerVo.getPassword()));
user.setClassroomId(classroom.getId());
user.setCreatetime(LocalDateTime.now());
user.setEnabled(1);
user.setLocked(0);
//执行User新增
int num=userMapper.insert(user);
//验证新增结果
if(num!=1) {
throw new ServiceException("服务器忙,稍后再试");
}
//将新增的用户赋予学生的角色(新增user_role的关系表)
UserRole userRole=new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2);
num=userRoleMapper.insert(userRole);
//验证关系表新增结果
if(num!=1) {
throw new ServiceException("服务器忙,稍后再试");
}
}
private final List<User> masters=
new CopyOnWriteArrayList<>();
private final Map<String,User> masterMap=
new ConcurrentHashMap<>();
private final Timer timer=new Timer();
//初始化块:在构造方法运行前开始运行
{
timer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (masters){
masters.clear();
masterMap.clear();
}
}
},1000*60*30,1000*60*30);
}
@Override
public List<User> getMasters() {
if(masters.isEmpty()){
synchronized (masters){
if(masters.isEmpty()){
QueryWrapper<User> query=new QueryWrapper<>();
query.eq("type",1);
//将所有老师缓存masters集合中
masters.addAll(userMapper.selectList(query));
for(User u: masters){
masterMap.put(u.getNickname(),u);
}
//脱敏:将敏感信息从数组(集合\map)中移除
for(User u: masters){
u.setPassword("");
}
}
}
}
return masters;
}
@Override
public Map<String, User> getMasterMap() {
if(masterMap.isEmpty()){
getMasters();
}
return masterMap;
}
// @Autowired
// IQuestionService questionService;
@Override
public UserVo currentUserVo(String username) {
//获得登录用户名
//String username=currentUsername();
//获得当前对象基本信息
UserVo user=userMapper.findUserVoByUsername(username);
// Integer questions=questionService
// .countQuestionsByUserId(user.getId());
// user.setQuestions(questions);
//用户收藏数信息未做!!!
return user;
}
}
需要注意!迁移过程中是否导入了正确的包中的类
否则可能导致错误
在sys项目中添加springsecurity的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
sys模块创建security包
包中新建SecurityConfig类代码如下
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().permitAll();
}
}
这个类是为了兼容今后在gateway中编写的安全设置而创建的,现在就是所有请求的放行的设置
测试创建业务逻辑层对象
@Resource
IUserService userService;
@Test
void testService(){
System.out.println(userService);
}
迁移控制层代码
编写统一异常处理类
在controller包中添加一个异常处理类
ExceptionControllerAdvice代码如下
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
//@ExceptionHandler表示这个方法时用来出处理异常的
@ExceptionHandler
public R handlerServiceException(ServiceException e){
log.error("业务异常",e);
return R.failed(e);
}
@ExceptionHandler
public R handlerException(Exception e) {
log.error("其它异常", e);
return R.failed(e);
}
}
在controller包中创建一个UserController类处理注册功能
其中的方法来着portal的SystemController中的registerStudent方法
代码如下
@RestController
@RequestMapping("/v1/users")
@Slf4j
public class UserController {
@Resource
private IUserService userService;
@PostMapping("/register")
public R registerStudent(
@Validated RegisterVo registerVo,
BindingResult validaResult) {
if (validaResult.hasErrors()) {
String error = validaResult.getFieldError()
.getDefaultMessage();
return R.unproecsableEntity(error);
}
System.out.println(registerVo);
log.debug("得到信息为:{}", registerVo);
userService.registerStudent(registerVo);
return R.created("注册成功!");
}
}
最后只需要修改一下gateway模块中用户注册时调用的register.js文件
中的ajax方法发送请求的路径即可
修改为/sys/v1/users/register
let app = new Vue({
el:'#app',
data:{
inviteCode:'',
phone:'',
nickname:'',
password:'',
confirm:'',
message:'',
hasError:false
},
methods:{
register:function () {
console.log('Submit');
let data = {
inviteCode: this.inviteCode,
phone: this.phone,
nickname: this.nickname,
password: this.password,
confirm: this.confirm
}
console.log(data);
if(data.password !== data.confirm){
this.message="两次密码输入不一致";
this.hasError=true;
return;
}
$.ajax({
url:"/sys/v1/users/register",
method: "POST",
data: data,
success: function (r) {
console.log(r);
if(r.code == CREATED){
console.log("注册成功");
console.log(r.message);
//注册成功,可以直接跳转到登录页
location.href="/login.html?register";
}else{
console.log(r.message);
//如果注册失败将信息显示在信息Div中
app.message=r.message;
app.hasError=true;
}
}
});
}
}
});
微服务之间的调用
Ribbon实现负载均衡与用户登录
什么是Ribbon
SpringCloud提供的实现了负载均衡的RPC客户端
RPC其实就是一个服务器调用另一个服务器提供的方法
负载均衡:能够自动分配每个服务器的压力尽量接近的效果
只要涉及到跨微服务的功能调用,我们就需要使用Ribbon的功能了
例如:
1.用户管理功能由sys模块提供
2.用户权限管理有gateway模块提供
使用Ribbon在Eureka的依赖添加后自动就引入了
不需要额外修改pom文件
Ribbon的使用
Ribbon的调用原理
我们看到上面的图中RestTemplate类型对象通过getForObject方法调用了其它服务器的方法,并接收到了它的返回值
这个方法可以写3个参数
参数1:url
指定被调用的Rest接口的路径
http://sys-service/v1/auth/demo
sys-service指Eureka注册的实例名称
/v1/auth/demo指这个服务的请求路径
参数2:type
返回值的类型声明,类型为反射
如果控制器返回的类型时List需要转换成数组来接收
参数3:
是一个可变参数,会发送的指定的Controller方法中作为这个方法的参数
使用Ribbon实现调用
先来编写服务的提供者
在sys模块编写一个AuthController类
代码如下
@RestController
@RequestMapping("/v1/auth")
@Slf4j
public class AuthController {
@GetMapping("/demo")
public String demo(){
return "Hello Ribbon!";
}
}
转到gateway模块
主方法所在类中添加一个注入
代码如下
@SpringBootApplication
@EnableZuulProxy
public class StrawGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(StrawGatewayApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
下面可以在测试类中进行调用测试
在gateway模块的test测试包中编写如下代码
@Resource
private RestTemplate restTemplate;
//测试跨微服务调用的功能
@Test
void contextLoads() {
String url="http://sys-service/v1/auth/demo";
String str=restTemplate.getForObject(url,String.class);
System.out.println(str);
}
开始迁移登录功能
迁移登录功能呢的步骤
1.从sys业务层代码(IUserService)获得用户和权限信息
2.在sys中的AuthController控制器中定义Rest接口,作为服务的提供者
3.gateway中添加依赖
4.gateway中添加UserDetailServiceImpl类提供认证和授权信息
5.添加SecurityConfig来配置访问规则
步骤1:
sys模块,IUserService中添加以下方法
//根据用户名获得用户信息
User getUserByUsername(String username);
//根据用户id获得用户权限
List<Permission> getUserPermissions(Integer userId);
//根据用户id获得角色
List<Role> getUserRoles(Integer userId);
步骤2:
在UserServiceImpl类中对接口中声明的方法进行实现
public User getUserByUsername(String username) {
return userMapper.findUserByUsername(username);
}
@Override
public List<Permission> getUserPermissions(Integer userId) {
return userMapper.findUserPermissionsById(userId);
}
@Override
public List<Role> getUserRoles(Integer userId) {
return userMapper.findUserRolesById(userId);
}
可以进行以下测试
代码如下
@Test
void testPermission(){
List<Permission> permissions=
userService.getUserPermissions(11);
for(Permission p : permissions){
System.out.println(p);
}
}
步骤3:
在AuthController类中编写3个方法,分别对应业务逻辑层中的三个方法
代码如下
@Resource
IUserService userService;
@GetMapping("/user")
public User getUser(String username){
return userService.getUserByUsername(username);
}
@GetMapping("/permissions")
public List<Permission> getPermissions(Integer userId){
return userService.getUserPermissions(userId);
}
@GetMapping("/roles")
public List<Role> getRoles(Integer userId){
return userService.getUserRoles(userId);
}
步骤4:
转到gateway模块
首先添加SpringSecurity的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
步骤5:
gateway模块要添加commons的依赖支持
代码如下
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>straw-commons</artifactId>
</dependency>
在impl包中新建一个UserDetailServiceImpl类
这个类提供认证和授权信息,注意用户信息来自sys模块
涉及到了跨微服务的调用
代码如下
@Component
@Slf4j
public class UserDetailServiceImpl
implements UserDetailsService {
@Autowired
RestTemplate restTemplate;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
String url="http://sys-service/v1/auth/user?username={1}";
User user=restTemplate.getForObject(
url,User.class,username);
if(user == null){
throw new UsernameNotFoundException("用户名密码不正确");
}
//跨服务查询用户所有权限
url="http://sys-service/v1/auth/permissions?userId={1}";
Permission[] permissions=restTemplate.getForObject(
url,Permission[].class,user.getId());
//跨服务查询用户所有角色
url="http://sys-service/v1/auth/roles?userId={1}";
Role[] roles=restTemplate.getForObject(
url,Role[].class,user.getId());
if(permissions==null || roles==null){
throw new UsernameNotFoundException("角色或权限缺失!");
}
//构建权限和角色的数组,最终赋值到Spring-Security中,用于认证
String[] auths=new String[permissions.length+roles.length];
int index=0;
for(Permission p : permissions){
auths[index]=p.getName();
index++;
}
for(Role r:roles){
auths[index]=r.getName();
index++;
}
//构建一个UserDetail对象,返回给Spring-Security
UserDetails u=
org.springframework.security
.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.disabled(user.getEnabled()==0)
.accountLocked(user.getLocked()==1)
.authorities(auths)
.build();
return u;
}
}