业务背景
Student表
CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8_bin DEFAULT NULL, `phone` varchar(11) COLLATE utf8_bin DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
两个业务功能
针对上述student表, 综合应用springboot, mybatis, dubbo, redis实现如下两个业务功能
1. 注册学生
-
要求:
-
注册接口定义为:int saveStudent(Student student)
-
利用传入的学生的手机号注册,手机号必须唯一
-
如果已经存在了手机号, 注册失败, 返回2
-
如果手机号为空,注册失败,返回-1
-
注册成功,返回0
2. 查询学生
- 要求:
- 查询接口定义为:Student queryStudent(Integer id)
- 根据id查询目标学生
- 先到redis查询学生,如果redis没有,从数据库查询
- 如果数据库有,把查询到的学生放入到redis,返回该学生,后续再次查询这个学生应该从redis就能获取到
- 如果数据库也没有目标学生,返回空
其他要求
- 关于Dubbo
- 要求使用dubbo框架,addStudent, queryStudent是由服务提供者实现的
- 消费者可以是一个Controller,调用提供者的两个方法, 实现学生的注册和查询
- 关于前端页面
- 页面使用html, ajax, jquery
- 通过postman发送post请求,来注册学生
- 通过html页面上的form表单,提供文本框输入id, 进行查询
- html, jquery.js都放到springboot项目的resources/static目录中
编程实现
项目结构
- 分布式总体项目结构
- 公共接口项目结构
- 服务提供者项目结构
- 消费者项目结构
dubbo的公共接口项目
注意该项目为普通的maven项目即可
实体类
package com.example.demo.model; import java.io.Serializable; public class Student implements Serializable { private static final long serialVersionUID = -3272421320600950226L; private Integer id; private String name; private String phone; private Integer age; @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + ''' + ", phone='" + phone + ''' + ", age=" + age + '}'; } //防止缓存穿透,可以获取默认学生(学生信息故意设置不合法,后期在redis中一眼就能看出来是异常数据),填充到redis中 public static Student getDefaultStudent(){ Student student = new Student(); student.setId(-1); student.setName("-"); student.setPhone("-"); student.setAge(0); return student; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Student(Integer id, String name, String phone, Integer age) { this.id = id; this.name = name; this.phone = phone; this.age = age; } public Student() { } }
提供的服务接口定义
package com.example.demo.service; import com.example.demo.model.Student; public interface StudentService { //保存学生信息 int saveStudent(Student student); //根据id,查询学生信息 Student queryStudent(Integer id); }
dubbo的服务提供者项目
注意:该项目为springboot项目,且在起步依赖里要勾选web(web依赖可以不选), redis, mysql, mybatis的起步依赖
项目配置
- 额外在pom.xml里手动加入对公共接口项目以及dubbo和zookeeper的依赖
<!-- 公共项目依赖 --> <dependency> <groupId>com.example.demo</groupId> <artifactId>demo-api</artifactId> <version>1.0.0</version> </dependency> <!--dubbo依赖--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.8</version> </dependency> <!--zookeeper依赖--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-dependencies-zookeeper</artifactId> <version>2.7.8</version> <type>pom</type> <exclusions> <!-- 排除log4j的依赖--> <exclusion> <artifactId>slf4j-log4j12</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency>
- 配置application.properties文件
########################### 配置dubbo #配置提供的服务名称 dubbo.application.name=student-service-provider #配置需要扫描的包 dubbo.scan.base-packages=com.example.demo.service #配置注册中心 dubbo.registry.address=zookeeper://127.0.0.1:2181 ########################### 配置redis #redis服务的ip spring.redis.host=127.0.0.1 #redis服务的端口 spring.redis.port=6379 ########################### mybatis配置 #mybatis中mapper文件编译到的资源路径 mybatis.mapper-locations=classpath:mapper/*.xml #mybatis日志输出 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl ############################ 数据源配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://数据库服务器ip:3306/数据库名?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 spring.datasource.username=XXX spring.datasource.password=YYY
dao层
- dao接口
package com.example.demo.dao; import com.example.demo.model.Student; import org.apache.ibatis.annotations.Param; public interface StudentDao { //以手机号作为查询条件,判断学生是否存在 Student queryStudentByPhone(@Param("phone") String phone); //保存新创建的学生信息 int saveStudent(Student student); //根据学生id,查询学生信息 Student queryStudentById(@Param("id") Integer id); }
- dao接口对应的xml文件, 位于resources/mapper目录下,这里将dao接口和dao.xml文件分开管理
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.dao.StudentDao"> <!-- 以学生手机号码为依据,判断学生是否已经存在 --> <select id="queryStudentByPhone" parameterType="string" resultType="com.example.demo.model.Student"> select id, name, age, phone from student where phone = #{phone} </select> <!-- 保存新创建的学生 --> <insert id="saveStudent" parameterType="com.example.demo.model.Student"> insert into student(name, age, phone) values(#{name}, #{age}, #{phone}) </insert> <!-- 根据学生id,查询学生信息 --> <select id="queryStudentById" parameterType="int" resultType="com.example.demo.model.Student"> select id, name, age, phone from student where id = #{id} </select> </mapper>
- 实现公共接口工程里对外提供的服务
package com.example.demo.service.impl; import com.example.demo.dao.StudentDao; import com.example.demo.model.Student; import com.example.demo.service.StudentService; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import javax.annotation.Resource; @DubboService(interfaceClass = StudentService.class, version = "1.0.0", timeout = 5000) public class StudentServiceImpl implements StudentService { @Resource private StudentDao studentDao; @Resource private RedisTemplate redisTemplate; //保存新创建的学生 @Override public int saveStudent(Student student) { int saveResult = 0;//表示保存学生信息的结果:1/添加成功 -1:手机号为空 2:手机号码重复 if(student.getPhone() == null){ saveResult = -1; }else{ Student queryStudentResult = studentDao.queryStudentByPhone(student.getPhone()); if(queryStudentResult != null){ saveResult = 2; }else{ //该学生尚未存在,保存到数据库中 saveResult = studentDao.saveStudent(student); } } return saveResult; } @Override public Student queryStudent(Integer id) { redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Student.class)); final String STUDENT_USER_KEY = "STUDENT:"; String key = STUDENT_USER_KEY + id; //先尝试从缓存获取:按照key的格式来查 Student student = (Student) redisTemplate.opsForValue().get(key); System.out.println("------- 从redis中查询数据 ----------> : " + student); if(student == null){ //缓存中没有,需要到数据库查询:按照id格式来查询 student = studentDao.queryStudentById(id); System.out.println("------- 从数据库中查询数据 ---------> : " + student); if(student != null){ //数据库中有该数据,存一份数据到redis中:按照key的格式来存 redisTemplate.opsForValue().set(key, student); }else{ //防止缓存穿透:对既未在缓存又未在数据库中的数据,设置默认值 redisTemplate.opsForValue().set(key, Student.getDefaultStudent()); } } return student; } }
- springboot主启动类上添加支持dubbo的注解并添加对dao接口扫描的注解
package com.example; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableDubbo @MapperScan(basePackages = "com.example.demo.dao") public class StudentserviceProviderApplication { public static void main(String[] args) { SpringApplication.run(StudentserviceProviderApplication.class, args); } }
dubbo消费者项目
该项目为springboot项目,启动项依赖只要勾选web依赖
pom.xml中的额外依赖均与服务提供者相同
项目配置
- 配置application.properties
#springboot服务的基本配置 server.port=9090 server.servlet.context-path=/demo #springboot中使用dubbo的配置 #消费者名称 dubbo.application.name=student-service-consumer #配置需要扫描的包 dubbo.scan.base-packages=com.example.demo.controller #配置注册中心 dubbo.registry.address=zookeeper://127.0.0.1:2181
- 同样在springboot的启动类上添加支持dubbo的注解
package com.example; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableDubbo public class StudentConsumerApplication { public static void main(String[] args) { SpringApplication.run(StudentConsumerApplication.class, args); } }
controller层
- 消费者与前端交互的controller层, 采用RESTful接口风格
package com.example.demo.controller; import com.example.demo.model.Student; import com.example.demo.service.StudentService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class StudentController { @DubboReference(interfaceClass = StudentService.class, version = "1.0.0") private StudentService studentService; @PostMapping("/student/add") public String addStudent(Student student){ int saveStudentResult = studentService.saveStudent(student); String msg = ""; if(saveStudentResult == 1){ msg = "添加学生: " + student.getName() + " 成功"; }else if(saveStudentResult == -1){ msg = "手机号不能为空"; }else if(saveStudentResult == 2){ msg = "手机号: " + student.getPhone() + " 重复,请更换手机号后重试"; } return msg; } @PostMapping("/student/query") public String queryStudent(Integer id){ String msg = ""; Student student = null; if(id != null && id > 0){ student = studentService.queryStudent(id); if(student != null){ msg = "查询到的学生信息: " + student.toString(); }else{ msg = "未查询到相关信息"; } }else{ msg = "输入的id范围不正确"; } return msg; } }
前端页面
前端html页面和js文件位于resources/static目录下
-
可以借助postman便捷的发送post请求来添加学生
-
查询学生的请求可以借助如下query.html页面,通过ajax来发送查询请求
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>query.html</title> <script src="../js/jquery-1.11.1-min.js"></script> <script type="text/javascript"> $(function (){ $("#stuBtn").click(function (){ var id = $("#stuId").val(); $.ajax({ url: "/demo/student/query", data:{"id":id}, type:"post", dataType:"text", success:function (data){ alert(data); } }) }); }); </script> </head> <body> <input type="text" id="stuId"><br> <input type="button" id="stuBtn" value="查询"> </body> </html>