Spring Cloud(2)

从单体项目到微服务

上次课网关项目的路由原理

在这里插入图片描述

单体服务器拆分为微服务

主要拆分的目标就是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模块的几个部分

  1. 数据访问层
  2. 业务逻辑层
  3. 控制器
  4. 配置网关路由

迁移数据层

在这里插入图片描述

复制完毕之后

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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值