项目前置配置 IDEA设置热部署 2022版本IDEA配置参考博客
step1: setting->Compiler
step2: setting-Advance Settings
实现代码关联远程git仓库 实际场景中,完成一个小功能应当及时提交远程仓库
vcs->enable Version Control
:相当于git init
IDEA右上角出现git
相关的功能按钮
左下角git
可以查看对应的信息,比如日志、未提交的文件等
在Terminal
下关联自己的github`执行以下操作
1 2 3 4 5 6 7 8 9 10 11 12 # 关联自己的github(以前设置后无需重复设置,ssh key免登录) git config --global user.name xxx git config --global user.email xxx # 将其与远程仓库关联起来,其中origin是别名 git remote add origin 远程github地址(注意是ssh形式地址) # 提交至远程仓库 git push -u origin 分支名 # 删除关联远程仓库 git remote remove origin # 查看关联远程仓库列表 git remote -v
新建子模块 整个project
由不同的子Module
组成,project
下的pom.xml
只做模块管理
整个项目目录如下:
日志的相关配置 项目启动信息配置 改写启动类
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootApplication public class MemberApplication { private static final Logger LOG = LoggerFactory.getLogger(MemberApplication.class); public static void main (String[] args) { SpringApplication app = new SpringApplication (MemberApplication.class); Environment env = app.run(args).getEnvironment(); LOG.info("启动成功" ); LOG.info("地址:\thttp://127.0.0.1:{}" ,env.getProperty("server.port" )); } }
banner.txt在线生成工具
项目运行日志 在resource
目录下新建日志配置文件loggback-spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <property name ="PATH" value ="./log/memeber" > </property > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <Pattern > %d{hh:mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern > </encoder > </appender > <appender name ="TRACE_FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${PATH}/trace.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <FileNamePattern > ${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern > <timeBasedFileNamingAndTriggeringPolicy class ="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP" > <maxFileSize > 10MB</maxFileSize > </timeBasedFileNamingAndTriggeringPolicy > </rollingPolicy > <layout > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern > </layout > </appender > <appender name ="ERROR_FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > ${PATH}/error.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <FileNamePattern > ${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern > <timeBasedFileNamingAndTriggeringPolicy class ="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP" > <maxFileSize > 10MB</maxFileSize > </timeBasedFileNamingAndTriggeringPolicy > </rollingPolicy > <layout > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern > </layout > <filter class ="ch.qos.logback.classic.filter.LevelFilter" > <level > ERROR</level > <onMatch > ACCEPT</onMatch > <onMismatch > DENY</onMismatch > </filter > </appender > <root level ="ERROR" > <appender-ref ref ="ERROR_FILE" /> </root > <root level ="TRACE" > <appender-ref ref ="TRACE_FILE" /> </root > <root level ="INFO" > <appender-ref ref ="STDOUT" /> </root > </configuration >
使用Http Client完成测试接口 IDEA
自带Http Clinet
插件,只要新建.http
文件,即可发起http
请求
增加AOP打印请求参数和返回结果 AOP
和Interceptor
都可以实现此功能,但是Interceptor
只能处理Controller
层的处理结果,
在memeber
模块下新建AOP
打印日志类aspect.LogAspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package com.bang.train.member.aspect;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.support.spring.PropertyPreFilters;import jakarta.servlet.ServletRequest;import jakarta.servlet.ServletResponse;import jakarta.servlet.http.HttpServletRequest;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.multipart.MultipartFile;@Aspect @Component public class LogAspect { public LogAspect () { System.out.println("Common LogAspect" ); } private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class); @Pointcut("execution(public * com.bang..*Controller.*(..))") public void controllerPointcut () { } @Before("controllerPointcut()") public void doBefore (JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Signature signature = joinPoint.getSignature(); String name = signature.getName(); LOG.info("------------- 开始 -------------" ); LOG.info("请求地址: {} {}" , request.getRequestURL().toString(), request.getMethod()); LOG.info("类名方法: {}.{}" , signature.getDeclaringTypeName(), name); LOG.info("远程地址: {}" , request.getRemoteAddr()); Object[] args = joinPoint.getArgs(); Object[] arguments = new Object [args.length]; for (int i = 0 ; i < args.length; i++) { if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) { continue ; } arguments[i] = args[i]; } String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters (); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("请求参数: {}" , JSONObject.toJSONString(arguments, excludefilter)); } @Around("controllerPointcut()") public Object doAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters (); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("返回结果: {}" , JSONObject.toJSONString(result, excludefilter)); LOG.info("------------- 结束 耗时:{} ms -------------" , System.currentTimeMillis() - startTime); return result; } }
新建公共子模块common 微服务项目一般存在多个模块,每个模块对应一个服务,负责某一项功能,各个模块可能存在许多公共的代码和相同的依赖,此时为了减小代码冗余和代码管理和修改,我们在项目下新建一个子模块common
将公共代码放在此模块下
比如:工具类、拦截器、AOP、常量、枚举类、公共配置等
将公共的依赖包放在此模块下的pom
文件中
根目录下的pom
文件负责依赖包的版本管理
公共模块下的pom
文件负责管理需要导入的包
比如:上述AOP实现打印请求和返回结果的日志代码就可以移除到该模块下,但要注意,此时应该修改memeber
模块下启动类的扫描范围,即@ComponentScan("com.bang.train.*")
增加公共模块后的项目目录
新建网关模块 网关模块主要用于:路由转发、请求校验
网关模块的配置文件application.yaml
1 2 3 4 5 6 7 8 9 10 server: port: 8000 spring: cloud: gateway: routes: - id: memeber uri: http://127.0.0.1:8001 predicates: - Path=/member/**
本地数据库的构建 对于各个项目而言,最好能够做到配置专库专用 ,对于一个项目,新建对应数据库的同时,创建一个专门的用户,将该用户的权限局限于对本项目对应数据库的增删改查,避免影响服务器中其他数据库里的数据。
集成Mybatis持久层框架 引入相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 3.0.0</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency >
配置数据库连接
在application.yaml
配置文件中进行设置
集成Mybatis官方生成器 利用mybatis
框架,需要:编写持久层接口
->编写对应的mapper.xml
文件(需要手动编写对应的SQL语句)
以上过程需要耗费较多经历,为简化开发可以使用以下两种替代方案
Mybatis-Plus
第三方框架
Mybatis
+官方生成器
这里我们采用第二种方案Mybatis
+官方生成器
使用Mybatis
官方生成器步骤
新建一个新的maven
项目generator
在generator
项目的pom
文件中引入mybatis generator
自动生成代码插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <build > <plugins > <plugin > <groupId > org.mybatis.generator</groupId > <artifactId > mybatis-generator-maven-plugin</artifactId > <version > 1.4.0</version > <configuration > <configurationFile > src/main/resources/generator-config-business.xml</configurationFile > <overwrite > true</overwrite > <verbose > true</verbose > </configuration > <dependencies > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency > </dependencies > </plugin > </plugins > </build >
编写对应的配置文件src/main/resources/generator-config-member.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <context id ="Mysql" targetRuntime ="MyBatis3" defaultModelType ="flat" > <property name ="autoDelimitKeywords" value ="true" /> <property name ="beginningDelimiter" value ="`" /> <property name ="endingDelimiter" value ="`" /> <plugin type ="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" /> <plugin type ="org.mybatis.generator.plugins.ToStringPlugin" /> <commentGenerator > <property name ="suppressAllComments" value ="true" /> </commentGenerator > <jdbcConnection driverClass ="com.mysql.jdbc.Driver" connectionURL ="jdbc:mysql://localhost:3306/train_member?useUnicode=true& characterEncoding=utf8& useSSL=false" userId ="train_member" password ="wu123456" > </jdbcConnection > <javaModelGenerator targetProject ="../member/src/main/java" targetPackage ="com.bang.train.member.domain" /> <sqlMapGenerator targetProject ="../member/src/main/resources" targetPackage ="mapper" /> <javaClientGenerator targetProject ="../member/src/main/java" targetPackage ="com.bang.train.member.mapper" type ="XMLMAPPER" /> <table tableName ="member" domainObjectName ="Member" /> </context > </generatorConfiguration >
点击mybatis generator
对应maven插件,会生成对应的代码文件
生成的代码文件
com.bang.train.member.domain.Member
:数据库train_member
的member
表对应的java实体类
(PO)
com.bang.train.member.domain.MemberExample
:组装SQL语句中where
后面的条件对应的实体类;条件构建器,用于构建SQL语句中的各种条件
com.bang.train.member.mapper.MemberMapper
:持久层对应的接口
src/main/resources/mapper/memberMapper.xml
:对应的mapper.xml
文件,里面含有各种SQL
语句
注意:
以上四个文件一定不要去动,每次重新店家genartor maven插件,这四个文件都会被覆盖重写;如果官方生成器对应插件无法满足项目需求,自定义的代码应编写在新的文件里,千万不要直接在这四个文件后面追加。
会员注册接口开发 业务层 在com.bang.train.member.IMemberService
接口下新建抽象方法register
1 2 3 4 5 6 long register (String mobile) ;
在com.bang.train.member.MemberServiceImpl
类下实现对应抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public long register (String mobile) { MemberExample memberExample = new MemberExample (); memberExample.createCriteria().andMobileEqualTo(mobile); List<Member> memberList = memberMapper.selectByExample(memberExample); if (!CollUtil.isEmpty(memberList)){ throw new RuntimeException ("手机号已被注册" ); } Member member = new Member (); member.setId(System.currentTimeMillis()); member.setMobile(mobile); memberMapper.insert(member); return member.getId(); }
控制层 1 2 3 4 @PostMapping("/register") public long register (String mobile) { return memberService.register(mobile); }
编写http文件利用Http Client进行测试 1 2 3 4 POST http://localhost:8001/member/register Content-Type : application/x-www-form-urlencodedmobile=15823209537
封装请求参数和结果(此模块的代码个人认为没有电脑商城项目好)) 封装请求参数 对于每个功能模块,将对应的请求参数封装成一个实体类,注意实体类的属性名与请求参数名要一致,这样前端请求会自动映射到实体类对应属性值
新建member
模块下用户注册对应的请求参数实体类com.bang.train.member.req.MemberRegReq
1 2 3 4 5 6 @Data @AllArgsConstructor @NoArgsConstructor public class MemberRegReq { String mobile; }
封装响应结果 响应结果包含三大基本信息:响应状态、响应状态描述信息、响应数据
在common
模块新建公共响应实体类com.bang.train.common.resp.CommonResp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data @NoArgsConstructor @AllArgsConstructor public class CommonResp <T>{ public boolean success=true ; public String message; public T content; public CommonResp (T content) { this .content=content; } }
对应控制层代码修改 1 2 3 4 5 6 @PostMapping("/register") public CommonResp<Long> register (MemberRegReq req) { CommonResp<Long> commResp = new CommonResp <>(); commResp.setContent(memberService.register(req)); return commResp; }
统一异常处理(此模块的代码个人认为没有电脑商城项目好) 业务层根据业务逻辑和执行结果,会向上层抛出各种类型异常,控制层需要对异常进行处理,直接将异常抛给前端不友好,需要针对异常,转换成统一的响应结果数据格式,所以需要构建统一异常处理类,借助于Spring
的@ExceptionHandler
注解来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ControllerAdvice public class ControllerExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class); @ExceptionHandler(Exception.class) @ResponseBody public CommonResp<Void> handleException (Exception e) { CommonResp<Void> commResp = new CommonResp <>(); commResp.setSuccess(false ); commResp.setMessage(e.getMessage()); return commResp; } }
自定义异常 根据业务层的业务逻辑,自定义对应的异常类,可以考虑利用枚举类进行异常的管理
定义枚举类,统一管理自定义异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.bang.train.common.exception;public enum BusinessExceptionEnum { MEMBER_EXIST_ERROR("手机号已注册" ); private String desc; BusinessExceptionEnum(String desc) { this .desc = desc; } public String getDesc () { return desc; } public void setDesc (String desc) { this .desc = desc; } @Override public String toString () { return "BusinessExceptionEnum{" + "desc='" + desc + '\'' + '}' ; } }
自定义业务异常类
1 2 3 4 5 6 7 8 9 10 11 12 package com.bang.train.common.exception;import lombok.Data;@Data public class BusinessException extends RuntimeException { public BusinessExceptionEnum E; public BusinessException (BusinessExceptionEnum E) { this .E = E; } }
修改统一的异常处理模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.bang.train.common.controller;import com.bang.train.common.exception.BusinessException;import com.bang.train.common.resp.CommonResp;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice public class ControllerExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class); @ExceptionHandler(Exception.class) @ResponseBody public CommonResp<Void> handleException1 (Exception e) { CommonResp<Void> commResp = new CommonResp <>(); commResp.setSuccess(false ); commResp.setMessage("未知类型异常,请联系管理员" ); return commResp; } @ExceptionHandler(BusinessException.class) @ResponseBody public CommonResp<Void> handleException2 (BusinessException e) { CommonResp<Void> commResp = new CommonResp <>(); commResp.setSuccess(false ); commResp.setMessage(e.getE().getDesc()); return commResp; } }
集成校验框架Validation 在实际生产环境中,大多数情况下需要对用户的输入参数进行校验,比如校验输入是否有特殊字符、手机号位数是否正确等;当然,输入的校验也可在前端进行
校验框架Validation
的使用步骤
引入对应的pom依赖
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
通过注解方式进行参数校验
在member
模块中注册功能对应的请求参数实体类进行修改
1 2 3 4 5 6 7 8 @Data @AllArgsConstructor @NoArgsConstructor public class MemberRegReq { @NotBlank(message = "【手机号】不能为空") String mobile; }
在注册功能对应controller
类的请求处理方法上加上注解@Valid
让校验功能起效
1 2 3 4 5 6 @PostMapping("/register") public CommonResp<Long> register (@Valid MemberRegReq req) { CommonResp<Long> commResp = new CommonResp <>(); commResp.setContent(memberService.register(req)); return commResp; }
新增校验异常处理代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ExceptionHandler(Exception.class) @ResponseBody public CommonResp<Void> handleException1 (Exception e) { CommonResp<Void> commResp = new CommonResp <>(); commResp.setSuccess(false ); if (e instanceof BindException){ BindException be = (BindException) e; String message = be.getBindingResult().getAllErrors().get(0 ).getDefaultMessage(); commResp.setMessage(message); LOG.error("校验异常:{}" ,message); }else { commResp.setMessage("未知类型异常,请联系管理员" ); LOG.error("未知类型异常,请联系管理员" ); } return commResp; }
雪花算法 member
模块注册功能业务层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public long register (MemberRegReq req) { String mobile = req.getMobile(); MemberExample memberExample = new MemberExample (); memberExample.createCriteria().andMobileEqualTo(mobile); List<Member> memberList = memberMapper.selectByExample(memberExample); if (!CollUtil.isEmpty(memberList)){ throw new BusinessException (BusinessExceptionEnum.MEMBER_EXIST_ERROR); } Member member = new Member (); member.setId(System.currentTimeMillis()); member.setMobile(mobile); memberMapper.insert(member); return member.getId(); }
目前,新注册用户的ID
是用当前时间戳来表示,在高并发场景下存在非唯一性问题,因为同一时刻存在大量请求
目前的可采取的其他方法及其对应的问题
更改service
层注册功能代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public long register (MemberRegReq req) { String mobile = req.getMobile(); MemberExample memberExample = new MemberExample (); memberExample.createCriteria().andMobileEqualTo(mobile); List<Member> memberList = memberMapper.selectByExample(memberExample); if (!CollUtil.isEmpty(memberList)){ throw new BusinessException (BusinessExceptionEnum.MEMBER_EXIST_ERROR); } Member member = new Member (); member.setId(SnowUtil.getSnowflakeId()); member.setMobile(mobile); memberMapper.insert(member); return member.getId(); }