这是一篇笔者在学习JAVA时记录的一些笔记,留以备份
请大佬轻喷
一、lombok的使用
-
lombok主要实现的就是不需要再写getter/setter方法,通过添加@Data方法,它会将其自动生成
// 使用lombok后,不需要再写getter/setter方法 @Data public class User { private Long id; private String name; private Integer age; private String email; }
二、Spring-boot的功能
1,@MapperScan
- @MapperScan 意思是扫描com.example.demomptest.mapper下的所有mapper类作为Mapper映射文件
- @MapperScan指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类
- 也可以在每个mapper类加@Mapper来作为Mapper文件 , 但是文件多的时候就会变得非常麻烦
- 可以扫描多个包
// 通过MapperScan才能够找到mapper文件夹下动态生成的mapper接口
@MapperScan("com.example.demomptest.mapper")
2,@RequestMapping
**@RequestMapping表示共享映射,如果没有指定请求方式,将接收GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE、CONNECT所有的HTTP请求。**如下代码中:调用findAllHospitalSet()的完整路径为:/admin/hospital/hospitalSet/findAll,同理有removeHospitalSetById(@PathVariable Long id)的完整路径为:/admin/hospital/hospitalSet/{id}
// 补充:@RestController注解表示这是一个RESTful控制器
@RestController
@RequestMapping("/admin/hospital/hospitalSet")
@Api(tags = "医院设置管理")
public class HospitalSetController {
@Autowired
private HospitalSetService hospitalSetService;
@GetMapping("findAll")
public Result findAllHospitalSet() {
List<HospitalSet> hospitalSetList = hospitalSetService.list();
return Result.ok(hospitalSetList);
}
@ApiOperation(value = "逻辑删除医院设置")
@DeleteMapping("{id}")
public Result removeHospitalSetById(@PathVariable Long id) {
boolean flag = hospitalSetService.removeById(id);
if (flag) {
return Result.ok();
} else {
return Result.fail();
}
}
}
3,组合注解
@GetMapping
注解指定了一个处理HTTP GET请求的方法
@GetMapping("findAll")
public Result findAllHospitalSet() {
List<HospitalSet> hospitalSetList = hospitalSetService.list();
return Result.ok(hospitalSetList);
}
@PostMapping
注解指定了一个处理HTTP POST请求的方法,对于一些不应该展示给用户看的数据,应该使用POST传递。
@ApiOperation(value = "添加医院设置")
@PostMapping("saveHospitalSet")
public Result saveHospitalSet(@RequestBody(required = true) HospitalSet hospitalSet) {
hospitalSet.setStatus(1);
// 签名密钥
Random random = new Random();
hospitalSet.setSignKey(MD5.encrypt(System.currentTimeMillis() + "" + random.nextInt(1000)));
// 调用service的方法添加数据
boolean saveResult = hospitalSetService.save(hospitalSet);
return saveResult ? Result.ok() : Result.fail();
}
注意:如下代码中,findPage/{current}/{limit}中current和limit是必须的查询参数,而
@RequestBody(required = false) HospitalSetQueryVo hospitalSetQueryVo中HospitalSetQueryVo是可选的查询参数
(再Swagger中current和limit必填,HospitalSetQueryVo 中包括的参数不一定要填)
@PostMapping("findPage/{current}/{limit}")
public Result findPageHospitalSet(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) HospitalSetQueryVo hospitalSetQueryVo)
@PutMapping
put是从客户端向服务端发送数据,但与post不同的是,post真正的目的其实是执行大规模的替换操作,而不是更新操作。
如果是添加信息,倾向于用@PostMapping,如果是更新信息,倾向于用@PutMapping。两者差别并不大。
@ApiOperation("医院设置锁定和解锁")
@PutMapping("lockHospitalSet/{id}/{status}")
public Result lockHospitalSet(@PathVariable Long id,
@PathVariable Integer status) {
HospitalSet result = hospitalSetService.getById(id);
result.setStatus(status);
hospitalSetService.updateById(result);
return Result.ok(;
}
@DeleteMapping
使客户端通过HTTP DELETE请求来移除某个资源
@ApiOperation(value = "逻辑删除医院设置")
@DeleteMapping("{id}")
public Result removeHospitalSetById(@PathVariable Long id) {
boolean flag = hospitalSetService.removeById(id);
if (flag) {
return Result.ok();
} else {
return Result.fail();
}
}
@PatchMapping
patch的目的是对资源数据打补丁或局部更新
下列代码实现了:只有当修改的内容非null时,才在本地数据库中的对应属性上修改,如果客户端没有改动,则属性不动,即对资源数据进行一个局部属性的更新,不会直接对整个实体进行更新。
@PatchMapping("{orderId}")
public Order patchOrder(@PathVariable("orderId") Long orderId,
@RequestBody Order patch){
Order order = repo.findById(orderId).get();
if(patch.getDeliveryName()!=null){
order.setDeliveryName(patch.getDeliveryName());
}
return repo.save(order);
}
4,@RequestBody
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
最常用的使用请求体传参的无疑是POST请求,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。
在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
注:如果参数时放在请求体中,application/json传入后台的话,那么后台要用@RequestBody才能接收到;如果不是放在请求体中的话,那么后台接收前台传过来的参数时,要用@RequestParam来接收,或者形参前什么也不写也能接收。
@ApiOperation(value = "条件查询带分页")
@PostMapping("findPage/{current}/{limit}")
public Result findPageHospitalSet(@PathVariable long current,
@PathVariable long limit,
// 此处假设前端传递过来时为JSON数据,因此使@RequestBody来接收请求体中的数据
@RequestBody(required = false) HospitalSetQueryVo hospitalSetQueryVo){
// 根据current和limit创建page对象
Page<HospitalSet> page = new Page<>(current, limit);
// 构造条件,要求医院名字为模糊查询、医院编号为相等
QueryWrapper<HospitalSet> wrapper = new QueryWrapper<>();
String hosname = hospitalSetQueryVo.getHosname();
String hoscode = hospitalSetQueryVo.getHoscode();
if (!StringUtils.isEmpty(hosname)) {
wrapper.like("hosname", hospitalSetQueryVo.getHosname());
}
if (!StringUtils.isEmpty(hoscode)) {
wrapper.eq("hoscode", hospitalSetQueryVo.getHoscode());
}
// 调用方法实现分页查询
Page<HospitalSet> hospitalSetPage = hospitalSetService.page(page, wrapper);
return Result.ok(hospitalSetPage);
}
5,@ControllerAdvice
@ControllerAdvice用来声明一些全局性的东西
注:@ControllerAdvice 类中的方法会被所有 @Controller 类共享和调用,可以对整个应用程序中的控制器起作用,而 @Controller 类中的方法只能处理该类所负责的请求
配合 @ExceptionHandler
用于捕获Controller中抛出的不同类型的异常,从而达到异常全局处理的目的;
@ControllerAdvice
// @ResponseBody使这个方法可以输出,否则虽然回调用这个方法,但是不会输出对应值
@ResponseBody
public class GlobalExceptionHandler {
// 如果发生了Exception.class的异常,那么就会执行error方法
@ExceptionHandler(Exception.class)
public Result error(Exception e) {
e.printStackTrace();
return Result.fail();
}
}
同时,使用 @ExceptionHandler 也可以配合自定义异常:
@ControllerAdvice
// @ResponseBody使这个方法可以输出,否则虽然回调用这个方法,但是不会输出对应值
@ResponseBody
public class GlobalExceptionHandler {
// 自定义全局异常,针对一些可能Exception.class检测不到的异常
@ExceptionHandler(YyghException.class)
public Result error(YyghException e) {
e.printStackTrace();
return Result.fail();
}
}
此时自定义异常需要手动抛出:
try {
int a = 1 / 0;
} catch (Exception e) {
throw new YyghException("错误", 201); // 抛出的信息固定为("错误", 201)
}
配合@InitBinder
用于请求中注册自定义参数的解析,从而达到自定义请求参数格式的目的;
配合@ModelAttribute
表示此方法会在执行目标Controller方法之前执行 。
6,处理跨域问题(Access-Control-Allow-Origin)
所谓跨域就是指:
-
当主域名不同时,如:
http://www.123.com/index.html 调用 http://www.456.com/server.php (主域名不同:123/456,跨域)
-
当子域名不同时,如:
http://abc.123.com/index.html 调用 http://def.123.com/server.php (子域名不同:abc/def,跨域)
-
当端口不同时,如:
http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
-
当协议不同时,如:
http://www.123.com/index.html 调用 https://www.123.com/server.php (协议不同:http/https,跨域)
解决跨域的方法:为使用到的控制类添加上@CrossOrigin注解即可
@RestController
@RequestMapping("/admin/hospital/hospitalSet")
@Api(tags = "医院设置管理")
@CrossOrigin
public class HospitalSetController {
@Autowired
private HospitalSetService hospitalSetService;
@ApiOperation(value = "获取所有医院设置")
@GetMapping("findAll")
public Result findAllHospitalSet() {
// 调用service中的方法
List<HospitalSet> hospitalSetList = hospitalSetService.list();
return Result.ok(hospitalSetList);
}
}
其中:@CrossOrigin有两个属性:origins: 允许可访问的域列表、maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。
@CrossOrigin("http://domain.com","6000")
7,实体类
注意:类名对应表名,必须一致,否则查不到对应的数据表
注意:私有属性的属性名是按小驼峰的形式编写,在转换为数据库的属性名时会自动变为下划线形式
@Data
@ApiModel(description = "通知相关")
public class Notice extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "通知id")
@TableId(type = IdType.AUTO)
private Integer noticeId; // 对应数据库中的notice_id
@ApiModelProperty(value = "通知标题")
private String title;
@ApiModelProperty(value = "通知内容")
private String content;
@ApiModelProperty(value = "发布者")
private String editor;
}
8,使用Junit测试
步骤一:安装依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
步骤二:编写测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {AutoScheduleApplication.class}) // 需要使用依赖项的时候添加相应的启动类
class SubjectServiceTest {
@Autowired
private SubjectService subjectService;
@Test
void querySubjectWithTeachingTeacher() {
List<Subjects> subjectsList = subjectService.querySubjectWithTeachingTeacher("2022-2023", 1);
for (Subjects subjects : subjectsList) {
System.out.println(subjects);
}
}
}
9,拦截器
preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;
postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView ;
afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在请求到达Controller之前执行,可以在这里进行token的验证
// 假设token存储在请求头中的Authorization字段
String token = request.getHeader("Token");
System.out.println(token);
if (token == null || token.isEmpty()) {
// 如果token为空,返回错误响应或者重定向到登录页面
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "访问前请先登录");
response.sendRedirect("/home");
return false; // 中断请求链
}
// 可以添加其他验证逻辑,例如验证token的有效性等
return true; // 继续请求链
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 在Controller执行之后,视图渲染之前执行
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在视图渲染完成后执行,可以用于清理资源等操作
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并指定拦截的路径
registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**").excludePathPatterns("/home");
}
}
注意:使用拦截器的时候,如果需要注入,那么如果直接加@component会出现空指针异常,这是由于拦截器加载的时间点在springcontext之前,所以在拦截器中注入自然为null。因此,我们必须在配置类加载的同时加载拦截器,如下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public TokenInterceptor tokenInterceptor() {
return new TokenInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并指定拦截的路径
registry.addInterceptor(tokenInterceptor()).
addPathPatterns("/**").
excludePathPatterns("/home", "/*.html", "/*.js", "/*.css");
}
}
三、MyBatis-Plus
1,主键策略
使用**@TableId(type = IdType.AUTO)**作为id的注释方法
此时如果运行insert语句,那么id为一串唯一的19位的数字,这一串数字的生成由 IdType.XX决定
@Data
public class User {
@TableId(type = IdType.AUTO) // 生成的是一串自增的19位数字
private Long id;
private String name;
private Integer age;
private String email;
}
@TableId(type = IdType.AUTO) // 生成的是一串自增的19位数字
@TableId(type = IdType.ASSIGN_ID) // 生成的是一串由雪花算法生成的19位数字
@TableId(type = IdType.INPUT) // 生成的是一串由程序员使用setId指定的ID
@TableId(type = IdType.NONE) // 不生成
// 全局设置主键生成策略(写到资源文件夹下的application.properties中)
mybatis-plus.global-config.db-config.id-type=auto
2,修改
使用updateById()进行修改
注意:MyBatis-Plus中,修改数据是根据id修改数据,其SQL语句对应为:
UPDATE user SET name=? WHERE id=?
因此JAVA中对应的应为
@Test
public void updateInfo() {
User user = new User();
user.setId(1752600885147525122L); // 由于整数过大,因此需要L表示长整型
user.setName("芜湖");
userMapper.updateById(user);
}
3,自动填充
使用@TableField注解方法和MetaObjectHandler来实现:MyBatis-Plus执行插入或者更新等操作时自动执行的方法
// User.java
@Data
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
@TableField(fill = FieldFill.INSERT) // 插入的时候自动填充值
private Date createTime;
// @TableField(fill = FieldFill.UPDATE) // 只有在修改的时候才会自动填充值,一开始插入的时候没有值
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入的时候有值,修改的时候也有值
private Date updateTime;
}
// 加上@Component才能交给spring管理
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// MyBatis-Plus一旦执行添加操作,这个方法就执行
@Override
public void insertFill(MetaObject metaObject) {
// 当执行这个方法的时候,new Date()会把时间赋值给createTime
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
// MyBatis-Plus一执行修改操作这个方法就执行
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
4,乐观锁
原理:在数据库中设置一个字段(通常为:version(int类型))来作为乐观锁的校验字段。
通过设置一个自动填充来实现当插入一条数据后对version自动赋值1的方法
// User.java
@Version
@TableField(fill = FieldFill.INSERT) // 添加数据时为其自动填充version=1
private Integer version;
// MyMetaObjectHandler.java
@Override
public void insertFill(MetaObject metaObject) {
// 当执行这个方法的时候,new Date()会把时间赋值给createTime
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("version", 1, metaObject);
}
而后导入乐观锁插件(该插件会实现判断是否发生冲突等方法),如果要修改一个字段的值,则会调用version来判断当前是否可以执行(由于是先从数据库中获取数据再修改数据,因此当获得的version值要进行修改时和数据库中的不匹配时则说明在其修改之前就已经有人修改过了,因此更新失败,由下方的SQL语句可以更好的理解)。
// MpConfig.java
@Configuration
// 通过MapperScan才能够找到mapper文件夹下动态生成的mapper接口
@MapperScan("com.example.demo.mapper")
public class MpConfig {
// 乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
如果数据库的version字段的值与当前的值一样的时候就会执行以下SQL语句,并且version会自增。
UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, version=? WHERE id=? AND version=?
5,查询
-
根据id查询(selectById)
根据给定的Id值来查询数据库
@Test public void testOptimisticLocker() { User user = userMapper.selectById(1752629874620727298L); }
-
一次性查询出多个id对应的数据(selectBatchIds)
根据给定的数组(该数组中包含多个id)来查询数据库
@Test public void SelectMany() { List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 3)); }
-
根据给定条件查询(selectByMap)
定义一个map,根据map中放入的条件来查询数据库。注意:其结果是一个列表
@Test public void SelectByMap() { Map<String, Object> map = new HashMap<>(); map.put("name", "zhangsan"); map.put("age", 20); List<User> result = userMapper.selectByMap(map); }
-
分页查询(根据MyBatis-Plus的分页插件实现)
@Test public void pagination() { Page<User> page = new Page<User>(3, 3); Page<User> userPage = userMapper.selectPage(page, null); // 此处的null指的是条件 // 通过返回的对象得到分页的数据 long pages = userPage.getPages(); // 总页数 long current = userPage.getCurrent(); // 当前页 List<User> records = userPage.getRecords(); // 得到当前页的数据集合 long total = userPage.getTotal(); // 总记录数 boolean hasNext = userPage.hasNext(); // 是否有下一页 boolean hasPrevious = userPage.hasPrevious(); // 是否有下一页 }
// 注意,使用分页查询需要配置插件 // 分页插件(MpConfig.java) @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); }
6,逻辑删除
所谓逻辑删除指的就是:中有一个删除字段,每次删除的时候并不是物理上的从数据库中删除,而是修改这个删除字段,使其处于一种逻辑上被删除但是物理上没有被删除的状态。(比如:1位未删除,0未删除,可显示的商品都为1,当要下架时,将其置为0即可,并不真正的删除)
逻辑删除中默认:0表示未被删除,1表示已删除
SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0
使用逻辑删除需要先在实体类中声明:
// User.java
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
并且设置一个自动填充,为新插入的数据默认设为未被删除的状态
// MyMetaObjectHandler.java
this.setFieldValByName("deleted", 0, metaObject);
7,复杂查询(使用Wrapper实现)
-
使用ge、gt、le、lt、isNull、isNotNull
@Test /* QueryWrapper中可以使用的判断条件 ge:大于等于 gt:大于 le:小于等于 lt:小于 isNull:是否为空 isNotNull:是否不为空 */ public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.le("age", 20); // 第一个参数是数据库中的属性名,第二个参数是判断的条件值 userMapper.selectList(queryWrapper); // 将queryWrapper看作一个条件参数扔给selectList()即可 }
-
使用eq、ne
@Test /* QueryWrapper中可以使用的判断条件 eq:等于 ne:不等于 */ public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("age", 20); userMapper.selectList(queryWrapper); }
-
使用between、notBetween
@Test /* QueryWrapper中可以使用的判断条件 between:在某个区间内 notBetween:不在某个区间内 */ public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.between("age", 18, 20); // 注意:次序颠倒则结果不一样 userMapper.selectList(queryWrapper); }
-
使用like、notLike、likeLeft、likeRight
@Test /* QueryWrapper中可以使用的判断条件 (like可以参考数据库中的LIKE) like:%赖% notLike:NOT %赖% likeLeft:%赖 likeRight:赖% */ public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("name", "zhang"); userMapper.selectList(queryWrapper); }
-
使用orderBy、orderByDesc、orderByAsc
@Test /* QueryWrapper中可以使用的排序 orderBy:排序 orderByDesc:降序排序(大到小) orderByAsc:升序排序(小到大) */ public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("id"); userMapper.selectList(queryWrapper); } // 该方法也是实现了降序排序 public void selectByWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderBy(true, false, "id"); userMapper.selectList(queryWrapper); }
8,@ApiModel注解
用在接口相关的实体类上的注解,它主要是用来对使用该注解的接口相关的实体类添加额外的描述信息,并且常常和@ApiModelProperty注解配合使用。
@ApiModelProperty注解:作用在接口相关实体类的属性(字段)上的注解,用来对具体的接口相关实体类中的参数添加额外的描述信息,除了可以和 @ApiModel 注解关联使用,也会单独拿出来用。
@Data
@ApiModel(description = "医院设置")
@TableName("hospital_set")
public class HospitalSet extends BaseEntity {
@ApiModelProperty(value = "医院名称")
@TableField("hosname")
private String hosname;
}
9,@Transient
被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性
@ApiModelProperty(value = "其他参数")
@Transient //被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性
private Map<String, Object> param = new HashMap<>();
10,@TableName和@TableField
@TableName:在实体类上指定,指定实体类和数据库表的映射关系。当实体类的类名在转成小写后和数据库表名相同时,可以不指定该注解。
@TableField:在mybatis-plus中通过@TableField注解可以指定非主键的字段的一些属性,主要解决以下两个问题:
-
①. 解决数据库表跟实体类属性不对应的问题,对象中的属性名和表中的字段名不一致的情况(成员属性为非驼峰命名)
// 指明该实体的属性和数据库表字段不一致, 进行关联 @TableField("nick_name") private String nick;
-
②. 排除非表字段,实体的一个成员属性但是表中没有对应字段的情况
// 指明该属性在数据库表的字段中不存在,表示当前属性不是数据库的字段,但在实际项目中必须使用,指定后不会报错,如果不指定会报错 @TableField(exist = false) private String salary;
-
③. 不查询指定字段属性,指定某些表字段不在查询结果中出现
// 指明该属性在查询结果中不出现 @TableField(select = false) private String password;
-
④. 指定字段的填充策略
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入的时候有值,修改的时候也有值 private Date updateTime;
11,@JsonFormat
用对应的实体类来接收数据库查询出来的结果时就完成了时间格式的转换,再返回给前端时就是一个符合我们设置的时间格式了。例如:
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField("create_time")
private Date createTime;
格式为:
@JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8")
pattern:是你需要转换的时间日期的格式
timezone:是时间设置为东八区,避免时间在转换中有误差
@DateTimeFormat:使用spring mvc表单自动封装映射对象时,我们在对应的接收前台数据的对象的属性上加@DateTimeFormat就可以返回给数据库时,保证该数据格式为我们指定的时间格式。
@ApiModelProperty(value = "创建时间")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField("create_time")
private Date createTime;
注解@JsonFormat主要是后台到前台的时间格式的转换
注解@DataFormAT主要是前后到后台的时间格式的转换
12,BaseMapper
MyBatis-Plus自带BaseMapper接口,这个接口定义好了许多的基本数据库操作。
当Controller层需要进行增删改查操作时,可以通过直接调用这里的方法完成。
Controller层通过Service层继承的IService接口(MyBatis-Plus自带的)来调用Mapper层继承的BaseMapper接口,并且调用该接口中相应的方法。例如:
// HospitalSetController.java
@ApiOperation(value = "逻辑删除医院设置")
@DeleteMapping("{id}")
public Result removeHospitalSetById(@PathVariable Long id) {
boolean flag = hospitalSetService.removeById(id);
if (flag) {
return Result.ok();
} else {
return Result.fail();
}
}
// IService.class
default boolean removeById(Serializable id) {
return SqlHelper.retBool(this.getBaseMapper().deleteById(id));
}
// BaseMapper.class
int deleteById(Serializable id);
其中BaseMapper.class中拥有的方法有:
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> wrapper);
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
T selectOne(@Param("ew") Wrapper<T> queryWrapper);
Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}
四、微服务
1,微服务
所谓微服务就是将一个项目拆分为多个模块(服务),这些模块(服务)可以独立运行,并且每个模块(服务)都会占用线程。
五、EasyExcel
1,导出
步骤一:定义好类型
@Data
public class DictEeVo {
// value指的是导出到Excel时第一行各自的列(即标题)的内容,index指的是数据是在第几列(0指第一列)
@ExcelProperty(value = "id", index = 0)
private Long id;
@ExcelProperty(value = "上级id", index = 1)
private Long parentId;
@ExcelProperty(value = "名称", index = 2)
private String name;
@ExcelProperty(value = "值", index = 3)
private String value;
@ExcelProperty(value = "编码", index = 4)
private String dictCode;
}
步骤二:生成对应的Service和Service实现方法
// Service
void exportData(HttpServletResponse response);
// ServiceImpl
public void exportData(HttpServletResponse response) {
// 设置Content类型(excel)
response.setContentType("application/vnd.ms-excel");
// 设置字符集
response.setCharacterEncoding("utf-8");
// 设置导出excel的文件名
String fileName = "dict";
// 为了实现浏览器文件下载,因此需要设置以下代码
response.setHeader("Content-disposition",
"attachment;filename=" + fileName + System.currentTimeMillis() + ".xlsx");
// 查询所有数据字典的信息
List<Dict> dictList = baseMapper.selectList(null);
// 由于dictList中的类型不符合dictEeVo的类型,因此需要进行转换
List<DictEeVo> dictEeVos = new ArrayList<DictEeVo>();
for (Dict dict : dictList) {
DictEeVo dictEeVo = new DictEeVo();
// copyProperties(dict, dictEeVo)等同于dictEeVo.setXxx(dict.getXxx)
BeanUtils.copyProperties(dict, dictEeVo);
dictEeVos.add(dictEeVo);
}
// 调用easyExcel
try {
EasyExcel.write(response.getOutputStream(), DictEeVo.class)
.sheet("dict") // 工作簿名称
.doWrite(dictEeVos); // 写入的数据
} catch (IOException e) {
e.printStackTrace();
}
}
步骤三:在Controller层中设置导出方法
@ApiOperation("导出数据字典为Excel")
@GetMapping("exportDictData")
public Result exportDictData(HttpServletResponse response) {
dictService.exportData(response);
return Result.ok();
}
步骤四:在前端使用
<!-- 使用target="_blank"跳转到一个空白页来执行获取Excel文件 -->
<a class="export-data" :href="exportURL" target="_blank">
<el-button type="primary" @click="exportData()">导出</el-button>
</a>
2,导入
步骤一:定义好类型
@Data
public class DictEeVo {
// value指的是导出到Excel时第一行各自的列(即标题)的内容,index指的是数据是在第几列(0指第一列)
@ExcelProperty(value = "id", index = 0)
private Long id;
@ExcelProperty(value = "上级id", index = 1)
private Long parentId;
@ExcelProperty(value = "名称", index = 2)
private String name;
@ExcelProperty(value = "值", index = 3)
private String value;
@ExcelProperty(value = "编码", index = 4)
private String dictCode;
}
步骤二:生成对应的Service和Service实现方法(EasyExcel.read需要使用到步骤一中的index来分辨读取的数据)
// Service
void importData(MultipartFile multipartFile);
// ServiceImpl
public void importData(MultipartFile multipartFile) {
try {
EasyExcel.read(
// getInputStream():返回InputStream读取文件的内容
multipartFile.getInputStream(),
DictEeVo.class, // 读取Excel文件中的数据类型(由步骤一指定)
new DictListener(baseMapper)
).sheet().doRead();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
步骤三:在Controller层中设置导入方法
@ApiOperation("把Excel中的数据导入到数据库")
@PostMapping("importDictData")
public Result importDictData(MultipartFile multipartFile) {
dictService.importData(multipartFile);
return Result.ok();
}
步骤四:在前端使用
<!-- 注意,此处的name要与后端MultipartFile对应的名称一致,否则会报错NullPointerException -->
<el-upload name="multipartFile" class="upload-demo" drag :action="importURL" :on-success="uploadSuccess"
:on-error="error" multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">件大小不应大于500kb</div>
</template>
</el-upload>
六、Redis
1,使用Redis
-
启动服务:先启动redis-server.exe再启动redis-cli.exe
-
使用服务:
// 在.properties文件中配置redis #这个地址是redis服务的服务器地址(默认存在:127.0.0.1) spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.database=0 spring.redis.timeout=1800000 spring.redis.lettuce.pool.max-active=20 spring.redis.lettuce.pool.max-wait=-1 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-idle=5 spring.redis.lettuce.pool.min-idle=0
在需要使用的实现方法中配置对应注解可以实现:何时将数据缓存入redis、何时清空redis中的缓存数据
// 将数据缓存入redis @Override // 使用redis缓存数据,名字为dict,keyGenerator为生成路径 // 例如:使用getChildrenData(1)则redis中会缓存一条记录: // 1) "dict::com.syt.syt_project.service_cmn.service.impl.DictServiceImplgetChildrenData1" @Cacheable(value = "dict", keyGenerator = "keyGenerator") public List<Dict> getChildrenData(Long id) { QueryWrapper wrapper = new QueryWrapper<>(); // 注意,此处匹配父节点id说明是该id的子节点 wrapper.eq("parent_id", id); List<Dict> dictList = baseMapper.selectList(wrapper); // 向list集合中的每个dict对象设置hasChildren值表示是否有子节点 for (Dict dict : dictList) { Long dictId = dict.getId(); boolean isChild = this.isChildren(dictId); dict.setHasChildren(isChild); } return dictList; }
// 将redis中缓存的数据销毁 @Override // allEntries = true: 方法调用后清空所有缓存 @CacheEvict(value = "dict", allEntries = true) public void importData(MultipartFile multipartFile) { try { EasyExcel.read( // getInputStream():返回InputStream读取文件的内容 multipartFile.getInputStream(), DictEeVo.class, new DictListener(baseMapper) ).sheet().doRead(); } catch (IOException e) { throw new RuntimeException(e); } }
2,基本注解
@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
@Cacheable 的逻辑是:查找缓存 - 有就返回 -没有就执行方法体 - 将结果缓存起来;
@CachePut 的逻辑是:执行方法体 - 将结果缓存起来;
所以 @Cacheable 适用于查询数据的方法,@CachePut 适用于更新数据的方法。
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
3,基本操作
- keys *:查询所有缓存
- flushdb:清除数据库中的所有缓存
- flushall:清除所有缓存
- get Xxx:查询所对应键中的内容
4,RedisTemplate
// 从Redis中获取缓存数据(使用键值缓存)
String code = redisTemplate.opsForValue().get(phone);
// 向Redis添加缓存数据
方法一:添加键值和内容,第一个参数为键值,第二个参数为内容
redisTemplate.opsForValue().set(phone, code);
方法二:添加键值、内容、时间和时间单位,以下2和TimeUnit.MINUTES总体表达2分钟
redisTemplate.opsForValue().set(phone, code, 2, TimeUnit.MINUTES);
5,Docker中的Redis容器
步骤一:拉取Redis镜像
docker pull redis:latest
步骤二:运行Redis容器
docker run -itd --name redisData -p 6379:6379 redis
步骤三:运行容器
docker exec -it redisData /bin/bash
步骤四:启动redis
redis-cli
步骤三和步骤四可以使用Docker桌面端完成:进入redis容器中在Exec中执行以下语句即启动redis
redis-cli
七、Nginx
1,使用Nginx
解决多端口访问问题(假设:配置nginx端口为9001,配置医院设置端口为8201、数据字典为8202。通过nginx可以实现访问9001就可以同时访问到8202和8201端口,避免了访问多个端口)
// nginx.conf文件中添加配置
server {
listen 9001; // 总和的端口号
server_name localhost;
location ~ /hospital/ { // 正则判断(只要路径中包含hospital就使用对应的端口)例如:/admin/hospital/hospitalSet
proxy_pass http://localhost:8201;
}
location ~ /cmn/ { // 正则判断cmn(只要路径中包含cmn就跳转到对应的端口)例如:/admin/cmn/dict
proxy_pass http://localhost:8202;
}
}
2,基本操作
在nginx.exe所在的文件夹下打开终端
- 输入nginx.exe为启动服务
- 输入nginx -s stop为停止服务
3,Docker中的Nginx容器
八、MongoDB
先行操作:在启用或关闭windows功能中打开适用于Linux的Windows子系统和虚拟机平台,并且在Microsoft Store中安装一款Linux平台(例如:Ubuntu)
参考:Windows安装使用Docker,方便你的开发和部署(DockerDesktop篇)_windows安装docker-CSDN博客
1, 安装MongoDB
// 在Windons自带的Linux环境中配置
#拉取镜像(安装)
docker pull mongo:latest
#创建和启动容器
docker run --name mymongo -p 27017:27017 -v /home/mongo/mymongo:/data/db -d mongo
#–name 后面跟的是你给该运行容器起的一个名字
#-p 后面跟的是:端口(服务器外部端口:容器内部端口)
#-v 后面跟的是(本地持久化目录:容器内数据目录)注意这个目录是指Linux下的目录
#创建管理者
use admin //进入到admin数据库
db.createUser({user:"root",pwd:"root",roles:[{role:'root',db:'admin'}]})
#进入容器
docker exec -it mymongo/bin/bash
#使用MongoDB客户端进行操作
mongosh
> show dbs #查询所有的数据库
admin 0.000GB
config 0.000GB
local 0.000GB
而后再次需要进入mongo的时候只需要执行
#进入容器
docker exec -it mymongo/bin/bash
#使用MongoDB客户端进行操作
mongosh
2,使用MongoDB
如下表,要使用MongoDB的时候,需要先创建数据库,再创建数据表**(注意,只有创建了数据表,数据库才真正存在)**
SQL术语/概念 | MongoDB术语/概念 | 解释/说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
-
创建/使用数据库:
use Xxx
-
查询所有数据库
show dbs
-
删除当前使用的数据库
db.dropDatabase()
-
创建集合(关系型数据库表达方式:创建数据表)
db.createCollection("XXX");
-
针对集合的一些查询
注意:向不存在的集合中第⼀次加入数据时, 集合会被创建出来(例如:在Java中执行一条插入语句,但是此插入语句中的集合并没有事先在Mongo中创建,那么他也会自行创建出来,再把数据插入进去,而不是运行错误)
db.getCollection('集合名').find({'字段名':'字段属性'})
等价于:
db.‘集合名’.find('字段名':'字段属性')
3,Java通过MongoTemplate实现
步骤一:安装好依赖后需要在application.properties文件中配置好地址
# mongoDemoDB为数据表名
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/mongoDemoDB
步骤二:编写对应实现类
@Data
// @Document指明了为数据表中的行(即:数据表中的属性字段)
// 此处说明User集合(表)中有id,name,age,email,createDate字段(如果该集合不存在则会自行创建一个)
@Document("User")
public class User {
@Id
private String id;
private String name;
private Integer age;
private String email;
private String createDate;
}
步骤三:通过注入MongoTemplate,用其方法实现相应功能
@SpringBootTest
class MongoDemoApplicationTests {
// 注入mongoTemplate
@Autowired
private MongoTemplate mongoTemplate;
// 添加操作
@Test
public void demo1() {
User user = new User();
user.setName("li-si");
user.setAge(20);
user.setEmail("23@qq.com");
user.setCreateDate(System.currentTimeMillis() + "");
User userImportInfo = mongoTemplate.insert(user);
System.out.println(userImportInfo);
}
// 查询所有信息
@Test
public void findAll() {
List<User> userList = mongoTemplate.findAll(User.class);
System.out.println(userList);
}
// id查询
@Test
public void findById() {
User user = mongoTemplate.findById("65c78985f1360611914285c0",User.class);
System.out.println(user);
}
// 条件查询(使用自带的Query)
@Test
public void findUserByCondition() {
Query query = new Query(Criteria.where("name").is("li-si")
.and("age").is(20));
List<User> userList = mongoTemplate.find(query, User.class);
System.out.println(userList);
}
// 模糊查询(使用正则表达式)
@Test
public void findUserByBlur() {
String regex = "^.*si.*$";
Pattern pattern = Pattern.compile(regex,Pattern.CASE_INSENSITIVE);
Query query = new Query(Criteria.where("name").regex(pattern));
List<User> userList = mongoTemplate.find(query, User.class);
System.out.println(userList);
}
// 分页查询
@Test
public void findUserListByPage(){
int pageNo = 2;
int pageSize = 3;
Query query = new Query(Criteria.where("name").is("zhang-san"));
long count = mongoTemplate.count(query,User.class);
// skip(n)表示跳过n条记录,limit(n)表示显示前n条记录
List<User> userList = mongoTemplate.find(
query.skip((pageNo - 1) * pageSize).limit(pageSize),
User.class);
System.out.println(userList);
System.out.println(count);
}
// 更新操作
@Test
public void updateUser() {
// 根据Id查询出相应数据
User user = mongoTemplate.findById("65c78985f1360611914285c0", User.class);
// 修改根据id得到的User的值
user.setName("wang-wu");
user.setAge(18);
// 设置更改的条件(id匹配)
Query query = new Query(Criteria.where("_id").is(user.getId()));
// 设置修改的内容
Update update = new Update();
update.set("name",user.getName());
update.set("age",user.getAge());
// 完成修改操作
UpdateResult upsert = mongoTemplate.upsert(query, update, User.class);
// 获取是否修改成功
long modifiedCount = upsert.getModifiedCount();
System.out.println(modifiedCount);
}
// 删除操作
@Test
public void deleteUser() {
User user = mongoTemplate.findById("65c788f825ad76711120836e", User.class);
Query query = new Query(Criteria.where("_id").is(user.getId()));
DeleteResult deleteResult = mongoTemplate.remove(query, User.class);
// 获取是否删除成功
System.out.println(deleteResult.getDeletedCount());
}
}
4,Java通过MongoRepository实现
步骤一:安装好依赖后需要在application.properties文件中配置好地址
# mongoDemoDB为数据表名
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/mongoDemoDB
步骤二:编辑好相应的Repository并且通过继承调用CRUD方法
public interface UserRepository extends MongoRepository<User,String> {
}
步骤三:注入Repository,并调用方法
@SpringBootTest
class mongoDemoApplicationTestRepository {
// 注入Repository
@Autowired
private UserRepository userRepository;
// 插入用户数据
@Test
public void insertUser() {
User user = new User();
user.setName("chen-liu");
user.setAge(20);
user.setEmail("23@qq.com");
user.setCreateDate(System.currentTimeMillis() + " ");
User save = userRepository.save(user);
System.out.println(save);
}
// 查询所有记录
@Test
public void findAll() {
List<User> userList = userRepository.findAll();
System.out.println(userList);
}
// 根据id查询
@Test
public void findById() {
User user = userRepository.findById("65c8bd47133f1a21e6a56cf1").get();
System.out.println(user);
}
// 条件查询
@Test
public void findByConditon() {
User user = new User();
user.setName("li-si");
user.setAge(20);
Example<User> example = Example.of(user);
List<User> userList = userRepository.findAll(example);
System.out.println(userList);
}
// 模糊查询
@Test
public void findByBlur() {
// 设置匹配规则
ExampleMatcher matcher = ExampleMatcher.
matching().
// ExampleMatcher.StringMatcher.CONTAINING表示使用模糊查询
// 如果不加则说明使用的是完全匹配
withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING).
// 开启大小写忽略
withIgnoreCase(true);
User user = new User();
user.setName("Li");
// 同样是根据user来过滤数据,但是加上了模糊查询和忽略大小写的匹配规则
Example<User> example = Example.of(user,matcher);
List<User> userList = userRepository.findAll(example);
System.out.println(userList);
}
// 分页查询
@Test
public void findByPage() {
// 分页
// 注意:此处的page中0代表的是第一页
Pageable pageable = PageRequest.of(1, 3);
// findAll()中也可以指定查询条件
Page<User> users = userRepository.findAll(pageable);
users.forEach(user -> System.out.println(user));
}
// 修改操作
@Test
public void updateUser() {
User user = userRepository.findById("65c8bd47133f1a21e6a56cf1").get();
user.setName("wang-wu");
// 虽然修改和添加一样是用save方法,但是实际上会判断id是否有值,有值则进行修改,无值则进行添加
User saved = userRepository.save(user);
System.out.println(saved);
}
// 删除操作
@Test
public void deleteUser() {
userRepository.deleteById("65c8bd47133f1a21e6a56cf1");
}
}
补充:可以在Repository中使用以下规则来实现查询
@Repository
public interface HospitalRepository extends MongoRepository<Hospital, String> {
// 由于MongoRepository回根据名称自动添加相应方法,因此此处不再需要手写方法(注意命名规范)
Hospital getHospitalByHoscode(String hoscode);
}
5,框架
注意:
使用MongoDB的时候使用的是Controller层=>Service层=>Repository层。
而使用Mysql等关系型数据库时使用的是Controller层=>Service层=>Mapper层。
九、Nacos
使用Nacos可以实现远程调用其他模块的接口
1,使用前配置
步骤一:GitHub上下载该开源项目
https://github.com/alibaba/nacos/releases
// 下载完成后,可以通过进入nacos/bin/startup.cmd启动(Windows)
步骤二:使用可视化工具管理
http://localhost:8848/nacos
步骤三:安装依赖并且配置服务地址
// 安装依赖
<!-- 服务调用feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
// 配置服务地址
// service_hospital(调用方)
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
// service_cmn(被调用方)
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
// 配置调用模块service_hospital的启动类
// 调用方
@SpringBootApplication
@ComponentScan(basePackages = {"com.syt"})
// 使用@EnableDiscoveryClient就会使该程序运行之后自动在nacos中注册
@EnableDiscoveryClient
// 使用@EnableFeignClients可以使得从远程接口中找到服务并调用
@EnableFeignClients(basePackages = "com.syt")
public class ServiceHospApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceHospApplication.class, args);
}
}
// 配置被调用模块service_cmn的启动类
// 被调用方
@SpringBootApplication
@ComponentScan(basePackages = {"com.syt"})
@EnableDiscoveryClient
public class ServiceCmnApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceCmnApplication.class, args);
}
}
2,如何使用
假设:有service_hospital模块和service_cmn模块,若此时service_hospital中有医院数据,但是需要service_cmn中的一些省市区或者医院等级的数据,那么使用nacos+Feign就可以实现service_hospital访问service_cmn的接口从而调用方法实现获取所需要的数据。
用户调用service_hospital中的服务时,service_hospital通过Feign调用service_cmn以得到最终需要的数据呈现给用户
步骤一:编写被调用方service_cmn的方法
// 根据dictCode和value查询
@ApiOperation("根据dictCode和value查询字典")
@GetMapping("getDictByDictCodeAndValue/{dictCode}/{value}")
public String getDictByDictCodeAndValue(@PathVariable String dictCode,
@PathVariable String value) {
String dictName = dictService.getDictName(dictCode, value);
return dictName;
}
步骤二:(可)编写一个模块放置Feign,以实现访问其他服务接口
注意:service-cmn是不能够写为service_cmn,否则会出现名称非法的情况
// 这个@FeignClient中的内容是指要调用的那个模块名称
// 由对应模块下的application.properties中的spring.application.name定义
@FeignClient("service-cmn")
@Repository
public interface DictFeignClient {
// 根据dictCode和value查询
// 注意:此处的地址需要的是完整的路径
@GetMapping("/admin/cmn/dict/getDictByDictCodeAndValue/{dictCode}/{value}")
// 注意此时@PathVariable需要该变量对应的路径上的名称
public String getDictByDictCodeAndValue(@PathVariable("dictCode") String dictCode,
@PathVariable("value") String value);
}
步骤三:调用方通过Feign使用被调用方的接口
@Service
public class HospitalServiceImpl implements HospitalService {
@Autowired
private DictFeignClient dictFeignClient;
// 将医院等级信息封装入param中
private void setHospitalType(Hospital item) {
// 调用远程接口的方法getDictByDictCodeAndValue来获取医院等级信息(需要dictCode和对应的value)
String hosType = dictFeignClient.getDictByDictCodeAndValue("HosType", item.getHostype());
item.getParam().put("hosType", hosType);
}
}
十、Spring Cloud Gateway
1,实现过程
步骤一:配置依赖(注意,也需要引入nacos服务发现)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
步骤二:配置其基本信息以及所需要代理的端口
# 服务端口
server.port=80
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#设置路由id
#注意这个id指的是相应模块中的application.properties所配置的spring.application.name
spring.cloud.gateway.routes[0].id=service-hospital
#设置路由的uri(其中lb指负载均衡)
spring.cloud.gateway.routes[0].uri=lb://service-hospital
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates=Path=/*/hospital/**
#设置路由id
spring.cloud.gateway.routes[1].id=service-cmn
#设置路由的uri
spring.cloud.gateway.routes[1].uri=lb://service-cmn
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[1].predicates=Path=/*/cmn/**
1.路由id:路由的唯一标示
2.路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
3.路由断言(predicates):判断路由的规则,
4.路由过滤器(filters):对请求或响应做处理
步骤三:配置启动类
@SpringBootApplication
public class ServerGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ServerGatewayApplication.class, args);
}
}
步骤四:配置跨域访问(配置完跨域访问后就不需要在使用@CrossOrigin,如果还是用@CrossOrigin则会出错)
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
十一、RabbitMQ
1,RabbitMQ简介
RabbitMQ是一款使用Erlang语言开发的,实现AMQP(高级消息队列协议)的开源消息中间件
- 可靠性。支持持久化,传输确认,发布确认等保证了MQ的可靠性。
- 灵活的分发消息策略。这应该是RabbitMQ的一大特点。在消息进入MQ前由Exchange(交换机)进行路由消息。分发消息策略有:简单模式、工作队列模式、发布订阅模式、路由模式、通配符模式。
- 支持集群。多台RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
- 多种协议。RabbitMQ支持多种消息队列协议,比如 STOMP、MQTT 等等。
- 支持多种语言客户端。RabbitMQ几乎支持所有常用编程语言,包括 Java、.NET、Ruby 等等。
- 可视化管理界面。RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker。
- 插件机制。RabbitMQ提供了许多插件,可以通过插件进行扩展,也可以编写自己的插件。
2,MQ(MessageQueue)主要功能
- **削峰:**假设系统A在某一段时间请求数暴增,有5000个请求发送过来,系统A这时就会发送5000条SQL进入MySQL进行执行,MySQL对于如此庞大的请求当然处理不过来,MySQL就会崩溃,导致系统瘫痪。如果使用MQ,系统A不再是直接发送SQL到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃。
- **异步:**一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。
- **解耦:**假设有系统B、C、D都需要系统A的数据,于是系统A调用三个方法发送数据到B、C、D。这时,系统D不需要了,那就需要在系统A把相关的代码删掉。假设这时有个新的系统E需要数据,这时系统A又要增加调用系统E的代码。为了降低这种强耦合,就可以使用MQ,系统A只需要把数据发送到MQ,其他系统如果需要数据,则从MQ中获取即可。
3,RabiitMQ的安装
docker pull rabbitmq:management
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
管理后台:http://localhost:15672
4,RabbitMQ的使用
步骤一:安装依赖
<dependencies>
<!--rabbitmq消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
步骤二:配置路由器、交换机、队列常量
public class MqConst {
/**
* 预约下单
*/
// RabbitMQ的DIRECT交换机名称
public static final String EXCHANGE_DIRECT_ORDER = "exchange.direct.order";
// RabbitMQ的DIRECT交换机和队列绑定的匹配键
public static final String ROUTING_ORDER = "order";
// RabbitMQ的队列名称
public static final String QUEUE_ORDER = "queue.order";
}
步骤三:所需要使用RabbitMQ的生产者或消费者模块中的配置需要添加以下配置:
#rabbitmq地址
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest // 通过http://localhost:15672登录的账号
spring.rabbitmq.password=guest // 通过http://localhost:15672登录的密码
步骤四:生产者使用RabbitTemplate发送消息,该消息将会传到消息队列中
// NoticeController.java
@PostMapping("insertNotice")
@ApiOperation(value = "发送通知")
public Result insertNotice(@RequestBody NoticeVo noticeVo) {
// Controller层调用rabbitService.sendMessage传入一个消息等待消费
boolean result = rabbitService.sendMessage(
MqConst.EXCHANGE_DIRECT_ORDER,
MqConst.ROUTING_ORDER,
noticeVo);
return result ? Result.ok() : Result.fail();
}
@Service
public class RabbitService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
*
* @param exchange 交换机
* @param routingKey 路由键
* @param message 消息
*/
public boolean sendMessage(String exchange, String routingKey, NoticeVo noticeVo) {
rabbitTemplate.convertAndSend(exchange, routingKey, noticeVo);
return true;
}
}
步骤五:消费者设置接收器监听队列,接收消息。当接收到消息后调用send方法发送通知
@Component
public class SmsReceiver {
@Autowired
private MsmService msmService;
// 监听QUEUE_MSM_ITEM所指队列,一旦监听到则调用send方法(发送短信)
@RabbitListener(queues = MqConst.QUEUE_ORDER)
public void send(NoticeVo noticeVo, Message message, Channel channel) {
msmService.send(noticeVo);
}
}
十二、阿里云
1,短信服务
实现过程:
步骤一:配置资源文件(假设该模块不需要使用数据库的情况下)
# 服务端口
server.port=8204
# 服务名
spring.application.name=service-msm
# 返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
# 最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 阿里云短信配置
aliyun.sms.regionId=default
aliyun.sms.accessKeyId=YOUR_ACCESS_KEY_ID
aliyun.sms.secret=YOUR_SECRET
步骤二:设置所需要的使用参数(这些参数很重要,因此不能直接暴露在外,因此使用封装思想封装数据)
@Component
public class ConstantPropertiesUtils implements InitializingBean {
// 调用配置文件application.properties中的值
@Value("${aliyun.sms.regionId}")
private String regionId;
@Value("${aliyun.sms.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.sms.secret}")
private String secret;
public static String REGION_ID;
public static String ACCESS_KEY_ID;
public static String SECRET;
@Override
public void afterPropertiesSet() throws Exception {
REGION_ID = regionId;
ACCESS_KEY_ID = accessKeyId;
SECRET = secret;
}
}
步骤三:配置Controller,实现调用发送手机验证码的服务
@RestController
@RequestMapping("/api/msm")
@Api(tags = "阿里云短信服务")
public class MsmApiController {
@Autowired
private MsmService msmService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 发送手机验证码
@GetMapping("send/{phone}")
@ApiOperation(value = "发送手机验证码")
public Result sendCode(@PathVariable String phone) {
// 从redis中获取验证码
String code = redisTemplate.opsForValue().get(phone);
// 若可以获取到则直接返回ok
if (!StringUtils.isEmpty(code)) {
return Result.ok();
}
// 若获取不到则使用阿里云短信服务生成验证码
// 获取六位验证码
code = RandomUtil.getSixBitRandom();
// 使用阿里云服务发送验证码短信,并且返回一个布尔值反馈是否发送成功
boolean isSend = msmService.send(phone, code);
if (isSend) {
redisTemplate.opsForValue().set(phone, code, 2, TimeUnit.MINUTES);
return Result.ok();
} else {
return Result.fail("发送短信失败");
}
}
}
使用阿里云短信服务实现发送短信
// 发送短信
@Override
public boolean send(String phone, String code) {
// 判断手机号是否为空
if (StringUtils.isEmpty(phone)) {
return false;
}
// 整合阿里云短信服务
// 设置相关参数
DefaultProfile profile = DefaultProfile.
getProfile(ConstantPropertiesUtils.REGION_ID,
ConstantPropertiesUtils.ACCESS_KEY_ID,
ConstantPropertiesUtils.SECRET);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
// 如果发送的请求是https时启用
//request.setProtocol(ProtocolType.HTTPS);
// 提交方式
request.setMethod(MethodType.POST);
// 域名
request.setDomain("dysmsapi.aliyuncs.com");
// 版本
request.setVersion("2017-05-25");
// 方法
request.setAction("SendSms");
//手机号
request.putQueryParameter("PhoneNumbers", phone);
//签名名称(注意,需要和模板信息一样才行)
request.putQueryParameter("SignName", "YOUR_SIGNATURE");
//模板code(注意,需要和模板信息一样才行)
request.putQueryParameter("TemplateCode", "YOUR_CODE");
//验证码,需要使用json格式
Map<String, Object> param = new HashMap();
param.put("code", code);
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));
//调用方法进行短信发送
try {
CommonResponse response = client.getCommonResponse(request);
System.out.println(response.getData());
return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
return false;
}
生成随机4位或者6位数字
public class RandomUtil {
private static final Random random = new Random();
private static final DecimalFormat fourdf = new DecimalFormat("0000");
private static final DecimalFormat sixdf = new DecimalFormat("000000");
// 获取四位验证码
public static String getFourBitRandom() {
return fourdf.format(random.nextInt(10000));
}
// 获取六位验证码
public static String getSixBitRandom() {
return sixdf.format(random.nextInt(1000000));
}
public static ArrayList getRandom(List list, int n) {
Random random = new Random();
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
// 生成随机数字并存入HashMap
for (int i = 0; i < list.size(); i++) {
int number = random.nextInt(100) + 1;
hashMap.put(number, i);
}
// 从HashMap导入数组
Object[] robjs = hashMap.values().toArray();
ArrayList r = new ArrayList();
// 遍历数组并打印数据
for (int i = 0; i < n; i++) {
r.add(list.get((int) robjs[i]));
System.out.print(list.get((int) robjs[i]) + "\t");
}
System.out.print("\n");
return r;
}
}
步骤四:判断传递过来的验证码是否和生成的验证码一致
// 某个使用需要的地方
// 由于是放在redis中,因此只要调用了获取手机验证码的api就可以通过redis获取到
String reidsCode = redisTemplate.opsForValue().get(phone);
if (!code.equals(reidsCode)) {
throw new YyghException(ResultCodeEnum.CODE_ERROR);
}
2,对象存储OSS
作用:将资源存储到公网上
实现过程:
步骤一:配置资源文件(假设该模块不需要使用数据库的情况下)
# 服务端口
server.port=8205
# 服务名
spring.application.name=service-oss
# 返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 阿里云oss(实现图片上传)
aliyun.oss.endpoint=YOUR_INFO1
aliyun.oss.accessKeyId=YOUR_INFO2
aliyun.oss.secret=YOUR_INFO3
aliyun.oss.bucket=YOUR_INFO4
步骤二:设置所需要的使用参数(这些参数很重要,因此不能直接暴露在外,因此使用封装思想封装数据)
@Component
public class ConstantPropertiesUtils implements InitializingBean {
// 调用配置文件application.properties中的值
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.secret}")
private String secret;
@Value("${aliyun.oss.bucket}")
private String bucket;
public static String END_POINT;
public static String BUCKET;
public static String ACCESS_KEY_ID;
public static String SECRET;
@Override
public void afterPropertiesSet() throws Exception {
END_POINT = endpoint;
BUCKET = bucket;
ACCESS_KEY_ID = accessKeyId;
SECRET = secret;
}
}
步骤三:在Controller中编写接口
@RestController
@RequestMapping("/api/oss/file")
@Api(tags = "阿里云存储服务OSS")
public class FileApiController {
@Autowired
private FileService fileService;
// 上传文件到阿里云OSS
@PostMapping("fileUpload")
@ApiOperation(value = "上传文件到阿里云")
public Result fileUpload(MultipartFile file) {
// 获取上传文件路径
String url = fileService.upload(file);
return Result.ok(url);
}
}
编写Service,实现上传文件到OSS的功能
@Service
public class FileServiceImpl implements FileService {
@Override
public String upload(MultipartFile file) {
// Endpoint
String endpoint = ConstantPropertiesUtils.END_POINT;
// 从环境变量中获取访问凭证
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.SECRET;
// 填写Bucket名称
String bucketName = ConstantPropertiesUtils.BUCKET;
try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 获取输入流
InputStream inputStream = file.getInputStream();
// 获取文件名称
String fileName = file.getOriginalFilename();
// 生成一个唯一的uuid,以防止文件重名
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
fileName = uuid + fileName;
// 按照当前日期创建文件夹,将文件上传到该文件夹中
String timeUrl = new DateTime().toString("yyyy/MM/dd");
// 使用/隔开,表示目录不同
fileName = timeUrl + "/" + fileName;
// 调用方法实现上传
PutObjectRequest putObjectRequest = new PutObjectRequest(
bucketName, // 要存放的bucket名称
fileName, // 路径名称(例如:a/b/xx.txt)
inputStream // 存放的数据
);
// 上传字符串。
PutObjectResult result = ossClient.putObject(putObjectRequest);
// 关闭OSSClient
ossClient.shutdown();
// 上传之后的路径
String url = "https://" + bucketName + '.' + endpoint + '/' + fileName;
System.out.println(url);
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
十三、Feign
微服务中,当一个模块需要调用另外一个模块的时候,可以使用Feign。只需要创建一个接口,然后添加注解即可使用Feign。
1,提供服务方
// 提供服务方(user)
@SpringBootApplication
@ComponentScan(basePackages = "com.eds")
// 使用@EnableDiscoveryClient就会使该程序运行之后自动在nacos中注册
@EnableDiscoveryClient
public class ServiceUserApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceUserApplication.class, args);
}
}
假设提供了一个获取token的接口
@GetMapping("getUserToken/{id}/{role}")
@ApiOperation("根据id和role查询信息")
public String getUserToken(@PathVariable Long id,
@PathVariable String role) {
String token;
switch (role) {
case "学生":
token = userInfoService.getStudentToken(id);
return token;
case "教师":
token = userInfoService.getTeacherToken(Math.toIntExact(id));
return token;
case "管理员":
token = userInfoService.getAdministratorToken(id);
return token;
}
return "null";
}
2,Feign
@FeignClient("service-user") // 服务提供方的name
public interface UserFeignClient {
/**
* @description: 根据id和role获取token
* @param: id, role
* @return: token
* @author 23769
* @date: 2024/3/13 15:23
*/
@GetMapping(value = "/api/user/getUserToken/{id}/{role}") // 注意:使用的是全路径
public String getUserToken(@PathVariable("id") Long id, // 最好加上("")
@PathVariable("role") String role);
}
3,使用服务方
// 使用服务方(auto_schedule)
@SpringBootApplication
@ComponentScan(basePackages = {"com.eds"})
// 使用@EnableDiscoveryClient就会使该程序运行之后自动在nacos中注册
@EnableDiscoveryClient
// 使用@EnableFeignClients可以使得从远程接口中找到服务并调用
@EnableFeignClients(basePackages = "com.eds")
调用Feign
switch (userId.toString().substring(0, 4)) {
case "2212":
case "2019":
userToken = userFeignClient.getUserToken(userId, "学生");
break;
case "2758":
userToken = userFeignClient.getUserToken(userId, "教师");
break;
case "2211":
userToken = userFeignClient.getUserToken(userId, "管理员");
break;
}
十四、GIT
1,工作流程
2,基础操作
git add .:将工作区(存放修改过文件或者新增文件)的文件提交到暂存区;
git commit -m “消息”:将暂存区的内容提交到仓库
git-log:查看对该仓库操作过的日志信息(不保护回滚信息);
git reflog:查看对该仓库操作过的所有日志信息(包括回滚操作);
git status:可以查看提交状态;
git reset --hard “版本号” :可以回滚仓库到某个提交;
3,忽略文件夹
通过touch .gitignore创建一个名为**.gitignore**的文件,在该文件中可以写入不想要git进行版本管理的文件。
例如:有许Xxx.a不需要管理,那么在**.gitignore中填写*.a即可使得以.a为后缀的文件的都不被管理**。
4,分支
创建分支
git branch:查看当前有什么分支;
git branch “分支名”:创建一个分支;
注意:对于每个用户只能指向一个分支。判断当前指向的分支的依据在于:HEAD ->,HEAD指向哪个,哪个就是当前使用的分支
git checkout “分支名”:切换分支;
git checkout -b “分支名”:创建并切换分支
合并分支
创建并且切换到新分支后,新分支会保存创建之前主分支的内容,此时如果新分支进行修改的话主分支并不会被修改,同理主分支进行修改的话新分支也不会被修改。
此时如果需要合并分支的话,那么就需要在需要合并的分支处(通常是master)输入git merge “需要合并的分支名”。
如下:主分支和新分支有一个文件不一样,此时主分支需要合并新分支的新文件。
删除分支
git branch -d “分支名”:检查后无误才执行删除分支;
git branch -D “分支名”:不做检查直接删除分支;
注意:当删除新分支时,如果新分支的文件没有全部都在主分支中,那么使用git branch -d "分支名"就会失败,因为他会做检查,这种情况下会认为你是否是误删除,如果想要继续执行这个操作就需要git branch -D “分支名”。
解决分支冲突
假设此时主分支修改了demoFeil01.text,新分支也修改了demoFeil01.text,当此时主分支需要执行合并新分支时就会出现分支冲突(即:判断不出来哪个是应该保留的),并且demoFeil01.text会变为下状态。
此时,只需要在demoFeil01.text中将需要保留的数据保留,其他的删除后,就可以再次执行git merge命令实现合并。
假设,我要保留主分支修改的,那么将上图修改为下图状态即可:
再执行合并即可(注意,这个处理冲突的操作并不会影响到新分支)。
企业常用分支流程
5,远程仓库
SSH密钥
步骤一:从base中生成SSH密钥
ssh-keygen -t rsa
步骤二:从base中获取公钥
cat ~/.ssh/id_rsa.pub
步骤三:往git仓库中添加公钥
步骤四:从base中查看是否成功
ssh -T git@gitee.com
使用GIT
步骤一:在base中添加远程仓库
注意:XXXX/git-demo.git是git中点击SSH生成的。
git remote add origin XXXX/git-demo.git
使用git remote可以查看当前连接的远程仓库
步骤二:推送主分支给git远程仓库
git push origin master
完整格式为:
git push [-f] [--set-upstream][远端名称[本地分支名][:远端分支名]]
/*
* -f 表示强制覆盖,即:不管之前的内容,强制推送当前的内容覆盖之前的内容
* --set-upstream 推送到远端的同时并且建立起和远端分支的关联关系,可以使用git branch -vv来显示关联关系
* 如果建立了关系之后(如下图),就可以使用简单的git push实现推送
*/
注意:执行步骤为:
git add .
git commit -m "Sss"
git push // 前提是有建立关联,否则使用git push [远端名称[本地分支名][:远端分支名]]
克隆
使用克隆指令,将远程仓库的文件以文件夹的形式克隆到当前目录下
git clone XXXX/git-demo.git
git clone XXXX/git-demo.git "指定文件夹名称"
抓取
当某一用户提交了新的代码到远程仓库之后,其他在同一远程仓库的用户如果想要获取该代码时,可以使用git fetch的方式来进行获取。
// 不指定[远程名称] [分支名称]时,抓取所有分支
git fetch [远程名称] [分支名称]
其他用户使用git fetch之后会出现下列情况:此时由于origin/master为最新的分支,而本地分支的master并没有被修改,因此还需要使用git merge origin/master来合并分支。
但是,为了简化操作,不再使用fetch后merge的方式,因此可以使用pull方法(pull方法是fetch和merge的结合体):
// 不指定[远程名称] [分支名称]时,抓取并更新所有分支
git pull [远程名称] [分支名称]
其结果为:
解决冲突
步骤一:冲突发生
注意,当一个用户已经修改file01.text并且推送(push)到远程仓库,此时另一个用户也修改了file01.text并且想要先拉取(pull)再推送(push)到远程仓库,那么后面拉取(pull)的那个用户就会遇到报错,此时就说明可能是遇到冲突,如下图所示。(如果是出现冲突的情况,git pull会报错,会说明哪个文件出现了冲突,此时就需要人工去解决)
步骤二:将下图修改下下图
步骤三:保存并推送分支
git add .
git commit -m "内容"
git push
当保存并推送分支后,该内容就以这个推送的分支为准。
6,终端命令行使用base.exe
以下为更改为base.exe之前的模样。
十六、Mybatis的XML形式
1,特殊字符处理
当在XML中需要使用 <(小于号)的时候会报错,如下图所示:
此时可以使用转义字符(适用于少量特殊字符需要处理的情景):
或者可以使用CDATA区(适用于大量特殊字符需要处理的情景):
2,返回结果
返回结果可能由于和指定实体类的属性名不一致而导致返回失败,因此有两种解决方案:resultType转化,resultMap转化。
resultType转化
通过设置别名或者搭配使用SQL片段来实现。
使用sql片段可以实现让一串sql代码可重用。
resultMap转化
通过resultMap片段来将不一致的属性名和类名转化,之后只要调用resultMap对应的Id即可。
3,条件判断
IF与WHERE
使用标签可以实现条件判断,不过有两种形式:
第一种:直接使用where
第二种:使用来自动判断
CHOOSE
使用可以实现类似switch的分支选择
另外可以在中使用来实现switch的default效果。
4,插入返回主键
5,动态更新
以下代码可以实现动态更新。
但是会出现两个问题,
一个是如果的逗号后面没有值,那么就会报错,因此逗号的问题。
一个是当条件全部不满足时,会出现
update Xx_table set where id = #{id}
的情况
根据这两个问题,可以通过设置解决,如下图所示:
6,批量删除
批量删除需要传来的参数是数组类型,因此可以根据来实现输出数组每一个值,转化为(1,2,3)形式,实现批量删除。
需要注意的是,有collection对应的值有两种。
一种是通过
boolean batchDeleteStudent(@Param("ids") Intenal[] idddd)
一种是通过直接**collection=“array”**的方式,collection此时会自动寻找到数组。
十七、AOP实现日志
1,设置注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LogAnnotation {
String content() default ""; // 内容
int sysType() default 0; // 系统类型(0管理平台,1用户平台)
int opType() default 0; // 操作类型(0登录,1增加,2删除,3修改,4查询,5查看)
String action() default ""; // 功能名称
}
2,配置Aspect
@Aspect
@Component
@EnableAsync
public class LogAspect {
@Resource
private LogMapper logMapper;
private String requestPath = null; // 请求地址
private long startTimeMillis = 0; // 开始时间
private long endTimeMillis = 0; // 结束时间
private String user = null; // 操作人
private HttpServletRequest request = null; //请求
/**
* 注解的位置
*/
@Pointcut("@annotation(com.eds.education_system.common.annotation.LogAnnotation)")
public void logPointCut() {
}
/**
* @param joinPoint
* @Description 前置通知,方法调用前触发,记录开始时间
*/
@Before(value = "logPointCut()")
public void before(JoinPoint joinPoint) {
startTimeMillis = System.currentTimeMillis();
}
public String getNameByToken(HttpServletRequest request) {
String token = request.getHeader("Token");
// 如果有token则从token中返回用户名
if (!StringUtils.isEmpty(token)) {
return JWPHelper.getUserName(token);
}
return "临时用户";
}
/**
* @param joinPoint
* @Description 后置通知,方法调用后触发,记录结束时间,操作人,入参等
*/
@After(value = "logPointCut()")
public void after(JoinPoint joinPoint) {
request = getHttpServletRequest();
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class<?> targetClass = null;
try {
targetClass = Class.forName(targetName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method[] methods = targetClass.getMethods();
String title;
String action;
Integer sysType;
Integer opType;
Class<?>[] clazzs;
for (Method method : methods) {
if (method.getName().equals(methodName)) {
clazzs = method.getParameterTypes();
if (clazzs != null && clazzs.length == arguments.length
&& method.getAnnotation(LogAnnotation.class) != null) {
request = getHttpServletRequest();
requestPath = request.getServletPath();
// 从token中获取名字
user = getNameByToken(request);
// 从注解处获取内容、操作类型、系统类型等数据
title = method.getAnnotation(LogAnnotation.class).content();
action = method.getAnnotation(LogAnnotation.class).action();
sysType = method.getAnnotation(LogAnnotation.class).sysType();
opType = method.getAnnotation(LogAnnotation.class).opType();
endTimeMillis = System.currentTimeMillis();
// 将日志插入数据库中
Log log = new Log(user, requestPath,
(endTimeMillis - startTimeMillis) + "ms",
"暂未开发", title, action, sysType, opType);
logMapper.insert(log);
}
}
}
}
/**
* @Description: 获取request
*/
public HttpServletRequest getHttpServletRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
return request;
}
/**
* @param joinPoint
* @return 环绕通知
* @throws Throwable
*/
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return null;
}
/**
* @param joinPoint
* @Description 异常通知
*/
public void throwing(JoinPoint joinPoint) {
System.out.println("异常通知");
}
}
额外知识
1,ServletResponse
客户端浏览器发出的请求被封装成一个httpServletRequest对象,该对象中包含了所有的信息包括请求的地址,请求的参数,提交的数据,上传的文件客户端的IP甚至客户端操作系统等。
一.获取客户端提交的数据
1.getParameter(Stringarg0):String
获取指定名称的参数值
2.getParameterMap():Map<String,String[]>
返回一个保存了请求消息中的所有参数名和值的Map对象。Map对象的key是字符串类型的参数名,value是这个参数所对应的object类型的值数组。
3.getParameterNames():Enumeration<String>
返回一个包含请求消息中的所有参数名的Enumeration对象
4.getParameterValues(Stringarg0):String[]
获取指定名称参数的所有值数组
5.getInputStream():ServletInputStream
获取请求的输入流中的数据
6.getSession():HttpSession
返回和客户端相关的session,若没有给客户端分配session,则返回null
7.getSession(boolean arg0):HttpSession
返回和客户端相关的session,若没有给客户端分配session,则创建一个session并返回
8.getCookies():Cookie[]
返回客户端发送的cookie
9.getAttributeNames():Enumeration<String>
返回当前请求的所有属性的名字集合
10.getAttribute(String arg0):Object
返回name指定的属性值
二.获取连接消息
1.getRemoteAddr():String
返回客户端的ip地址
2.getRemoteHost():String
返回客户端的主机名,若无法解析,则返回IP地址
3.getRemotePort():int
返回客户端的端口号
4.getLocalAddr():String
返回服务器的IP地址
5.getLocalName():String
返回服务器的主机名
6.getLocalPort():int
返回服务器的端口号
7.getServerName():String
返回http请求消息的host字段的主机号部分
8.getServerPort():int
返回http请求消息的host字段的端口号部分
9.getScheme():String
返回请求的协议名
10.getRequestURL():StringBuffer
返回完整的请求URL(?之前的部分)
11.getServerPath():String
获取请求的文件的路径
三.获取请求头消息
1.getHeader(String arg0):String
返回指定的头字段的值
2.getHeaders(String arg0):Enumeration<String>
返回重名头字段的值,该方法返回一个java.util.Enumeration
3.getHeaderNames():Enumeration<String>
返回一个包含所有头字段名字的Enumeration对象
4.getIntHeader(Stringarg0):int
返回指定的头字段的值,并将其转换为int
5.getDateHeader(Stringarg0):long
返回指定的头字段的值,并将其转换为long
6.getContentType():String
返回请求正文的内容类型
7.getContentLength():int
返回请求正文的长度,单位是字节,如未指定长度,则返回-1
8.getCharacterEncoding():String
返回请求正文的字符集编码。如没有指定,则返回null。
9.getReader():BufferedReader
获取请求体的数据流
四.获取请求行消息
1.getMethod():String
返回请求行中的请求方法(如get、post)
2.getRequestURI():String
返回请求行中的资源部分(不包括参数)
3.getQueryString():String
返回查询字符串(即请求行中的参数部分),该查询字符串在一个URL中由一个?引出,如果没有查询字符串,返回null。
4.getProtocol():String
返回请求行中的协议部分
5.getContextPath():String
返回该请求所属的Web应用的路径
2,HttpServletResponse
1.addCookie(Cookiearg0):void
将指定的cookie加入到当前的响应中
2.addDateHeader(Stringarg0,long arg1):void
用给定的名称和日期添加一个响应头
3.addHeader(Stringarg0,String arg1):void
将指定的名字和值加入到响应的头信息中
4.addIntHeader(Stringarg0,int arg1):void
添加一个给定名称的响应头和整数值
5.containsHeader(Stringarg0):boolean
该方法用于检查某个字段是否在响应消息头中存在,如果存在,则返回true,否则返回false。
6.encodeURL(Stringarg0):String
编码指定的URL
7.getOutputStream():ServletOutputStream
如果响应正文是非字符文本的数据或者是不需要进行编码转换的字符文本,建议使用字节输出流的方式返回响应正文。
8.getWriter():PrintWriter
如果响应正文全部是字符文本,并且需要进行编码转换,建议使用gatWriter方法。
从getWriter方法返回的PrintWriter对象不仅能很方便的向客户端返回相应正文,而且还会根据响应消息头的Content-Type所指定的字符编码格式自动转换响应正文(PrintWriter返回的响应正文是编码转换后的文本)。
9.sendError(intarg0):void
使用指定状态码发送一个错误到客户端
10.sendRedirect(Stringarg0):void
Arg0指定重定向的URL,该URL既可以是绝对的,也可以是相对的。该方法用更快捷的方式来设置响应状态码302,表示需要客户端重定向URL。
11.setCharacterEncoding(Stringarg0):void
这个方法实际上是设置Content-Type字段的字符集部分
注意:
在使用这个方法之前,如果Content-Type不存在,则必须先添加Content-Type,否则该方法设置的字符集类型也不会出现在响应消息头上。
12.setContentLength(intarg0):void
该方法用于设置相应正文的大小,单位是字节。Servlet引擎会根据向客户端实际输出的响应正文的大小自动设置Content-Length字段的值。
13.setContentType(Stringarg0):void
该方法设置Content-Type字段的值。(即设置MIME类型)
14.setDateHeader(Stringarg0,long arg1):void
将给出的名字和日期设置响应的头部
15.setHeader(Stringarg0,String arg1):void
将给出的名字和值设置响应的头部
16.setIntHeader(Stringarg0,int arg1):void
设置一个给定名称的响应头和整数值
17.setStatus(intarg0):void
给当前响应设置状态码
3,hospital-manage模块如何实现调用
步骤一:根据路径从Controller层中调用方法
@RequestMapping(value="/department/save",method=RequestMethod.POST)
public String save(String data, HttpServletRequest request) {
try {
apiService.saveDepartment(data);
} catch (YyghException e) {
return this.failurePage(e.getMessage(),request);
} catch (Exception e) {
return this.failurePage("数据异常",request);
}
return this.successPage(null,request);
}
步骤二:Service层实现方法
@Override
public boolean saveDepartment(String data) {
JSONArray jsonArray = new JSONArray();
if (!data.startsWith("[")) {
JSONObject jsonObject = JSONObject.parseObject(data);
jsonArray.add(jsonObject);
} else {
jsonArray = JSONArray.parseArray(data);
}
for (int i = 0, len = jsonArray.size(); i < len; i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("hoscode", this.getHoscode());
paramMap.put("depcode", jsonObject.getString("depcode"));
paramMap.put("depname", jsonObject.getString("depname"));
paramMap.put("intro", jsonObject.getString("intro"));
paramMap.put("bigcode", jsonObject.getString("bigcode"));
paramMap.put("bigname", jsonObject.getString("bigname"));
paramMap.put("timestamp", HttpRequestHelper.getTimestamp());
paramMap.put("sign", MD5.encrypt(this.getSignKey()));
// 由于科室信息由JSON数组组成,因此使用for循环遍历每一条JSON数据,每一条JSON数据都会调用另一个Service模块的对外接口
JSONObject respone = HttpRequestHelper.sendRequest(paramMap, this.getApiUrl() + "/api/hosp/saveDepartment");
System.out.println(respone.toJSONString());
if (null == respone || 200 != respone.getIntValue("code")) {
throw new YyghException(respone.getString("message"), 201);
}
}
return true;
}
// 科室信息
[
{"hoscode":"1000_0","depcode":"200050923","depname":"门诊部核酸检测门诊(东院)","intro":"门诊部核酸检测门诊(东院)","bigcode":"44f162029abb45f9ff0a5f743da0650d","bigname":"全部科室"},
{"hoscode":"1000_0","depcode":"200050924","depname":"国际医疗部门诊","intro":"国际医疗部门诊","bigcode":"44f162029abb45f9ff0a5f743da0650d","bigname":"全部科室"}
]
步骤三:调用对外接口
// 上传科室(存在于Service模块)
@ApiOperation("上传科室")
@PostMapping("saveDepartment")
public Result saveDepartment(HttpServletRequest request) {
// 获取客户端传递过来的医院信息
Map<String, String[]> requestMap = request.getParameterMap();
// 调整Map集合获取的数据类型
Map<String, Object> paramMap = HttpRequestHelper.switchMap(requestMap);
// 判断数据库和客户端两者的密钥是否相同
matchSignKey(paramMap);
// 该方法save方法实现将数据存进数据库
departmentService.save(paramMap);
return Result.ok();
}
Servlet引擎会根据向客户端实际输出的响应正文的大小自动设置Content-Length字段的值。
13.setContentType(Stringarg0):void
该方法设置Content-Type字段的值。(即设置MIME类型)
14.setDateHeader(Stringarg0,long arg1):void
将给出的名字和日期设置响应的头部
15.setHeader(Stringarg0,String arg1):void
将给出的名字和值设置响应的头部
16.setIntHeader(Stringarg0,int arg1):void
设置一个给定名称的响应头和整数值
17.setStatus(intarg0):void
给当前响应设置状态码
## 3,hospital-manage模块如何实现调用
步骤一:根据路径从Controller层中调用方法
@RequestMapping(value=“/department/save”,method=RequestMethod.POST)
public String save(String data, HttpServletRequest request) {
try {
apiService.saveDepartment(data);
} catch (YyghException e) {
return this.failurePage(e.getMessage(),request);
} catch (Exception e) {
return this.failurePage(“数据异常”,request);
}
return this.successPage(null,request);
}
步骤二:Service层实现方法
@Override
public boolean saveDepartment(String data) {
JSONArray jsonArray = new JSONArray();
if (!data.startsWith(“[”)) {
JSONObject jsonObject = JSONObject.parseObject(data);
jsonArray.add(jsonObject);
} else {
jsonArray = JSONArray.parseArray(data);
}
for (int i = 0, len = jsonArray.size(); i < len; i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("hoscode", this.getHoscode());
paramMap.put("depcode", jsonObject.getString("depcode"));
paramMap.put("depname", jsonObject.getString("depname"));
paramMap.put("intro", jsonObject.getString("intro"));
paramMap.put("bigcode", jsonObject.getString("bigcode"));
paramMap.put("bigname", jsonObject.getString("bigname"));
paramMap.put("timestamp", HttpRequestHelper.getTimestamp());
paramMap.put("sign", MD5.encrypt(this.getSignKey()));
// 由于科室信息由JSON数组组成,因此使用for循环遍历每一条JSON数据,每一条JSON数据都会调用另一个Service模块的对外接口
JSONObject respone = HttpRequestHelper.sendRequest(paramMap, this.getApiUrl() + "/api/hosp/saveDepartment");
System.out.println(respone.toJSONString());
if (null == respone || 200 != respone.getIntValue("code")) {
throw new YyghException(respone.getString("message"), 201);
}
}
return true;
}
// 科室信息
[
{“hoscode”:“1000_0”,“depcode”:“200050923”,“depname”:“门诊部核酸检测门诊(东院)”,“intro”:“门诊部核酸检测门诊(东院)”,“bigcode”:“44f162029abb45f9ff0a5f743da0650d”,“bigname”:“全部科室”},
{“hoscode”:“1000_0”,“depcode”:“200050924”,“depname”:“国际医疗部门诊”,“intro”:“国际医疗部门诊”,“bigcode”:“44f162029abb45f9ff0a5f743da0650d”,“bigname”:“全部科室”}
]
步骤三:调用对外接口
// 上传科室(存在于Service模块)
@ApiOperation(“上传科室”)
@PostMapping(“saveDepartment”)
public Result saveDepartment(HttpServletRequest request) {
// 获取客户端传递过来的医院信息
Map<String, String[]> requestMap = request.getParameterMap();
// 调整Map集合获取的数据类型
Map<String, Object> paramMap = HttpRequestHelper.switchMap(requestMap);
// 判断数据库和客户端两者的密钥是否相同
matchSignKey(paramMap);
// 该方法save方法实现将数据存进数据库
departmentService.save(paramMap);
return Result.ok();
}