文章目录
2. 创建服务
1. 创建项目
引用各种需要的包
dependencyManagement包引用管理,可以在父工程中配置,也可以在子工程中配置
下面这四个都用就完了:
-
spring-cloud-alibaba-dependencies
alibaba出的springcloud依赖管理,要使用alibaba的全家桶这个肯定少不了
-
spring-cloud-dependencies
springcloud官方的依赖,微服务架构下肯定都得用
-
spring-boot-dependencies
springboot的官方依赖,我们项目就是基于springboot开发,这个肯定少不了
-
lombok
简约化编程你离不了它,反正大家都在用
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
2. 使用mysql
2.1 mysql链接驱动
<!--mysql 链接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 采用最简单的JDBC链接-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 数据库配置-->
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/tk_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
@RestController
public class IndexController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/jdbc")
public Object jdbcStart(){
return jdbcTemplate.queryForList("select * from user_info");
}
}
# 查询结果
[
{
"id": 1,
"name": "张三",
"sex": 1,
"create_time": "2020-10-19T16:18:48.000+00:00",
"cretae_user": "sys",
"update_time": "2020-10-19T16:19:05.000+00:00",
"update_user": "sys",
"data_status": 1
}
]
2.2 采用阿里的druid链接池
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 数据库配置-->
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/tk_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
打开http://localhost:{port}/driud,可以看到数据库使用情况
durid数据库连接池,主要是做连接池最大化限制
3. 使用mybaits-plus实现数据库访问
为什么选择的是mybaits-plus,为什么不选择mybaits,MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
3.1
代码生成器
参考官网写代码生成
public class MybaitsGenerator {
static String projectPath = System.getProperty("user.dir");
/**
* 设置全局配置
* @return
*/
static GlobalConfig initGlobalConfig() {
GlobalConfig gc = new GlobalConfig();
//当前项目路径
gc.setOutputDir(projectPath + "/threeking-user/src/main/java");
gc.setAuthor("ah");
gc.setOpen(false);
gc.setSwagger2(true);
//是否覆盖 一般选择false
gc.setFileOverride(false);
gc.setDateType(DateType.ONLY_DATE);
return gc;
}
/***
* 设置数据源
* @return
*/
static DataSourceConfig initDataSourceConfig(){
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/tk_user?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
dsc.setUsername("root");
dsc.setPassword("123456");
return dsc;
}
/**
* 包配置
* @return
*/
static PackageConfig iniPackageConfig(){
PackageConfig pc = new PackageConfig();
pc.setParent("com.threeking.service");
pc.setModuleName("user");
//controller设置为空,则不生成controller
//pc.setController("");
pc.setEntity("entity");
return pc;
}
/**
* 配置生成策略
* @return
*/
static StrategyConfig initStrategyConfig(){
StrategyConfig sc = new StrategyConfig();
sc.setInclude("user_info");
sc.setNaming(NamingStrategy.underline_to_camel);
sc.setColumnNaming(NamingStrategy.underline_to_camel);
sc.setEntityLombokModel(true);
//sc.setEntityBuilderModel(true);
sc.setChainModel(true);
//逻辑删除
/**
* 修改1 为有效,0为无效
* mybatis-plus:
* global-config:
* db-config:
* logic-delete-value: 0 #逻辑已删除值(默认为1)
* logic-not-delete-value: 1 #逻辑已删除值(默认为0)
*/
//sc.setLogicDeleteFieldName("data_status");
sc.setRestControllerStyle(false);
return sc;
}
/**
* 自定义配置
* @return
*/
static InjectionConfig initInjectionConfig(String moduleName){
InjectionConfig cfg = new InjectionConfig(){
@Override
public void initMap() {
// to do nothing
}
};
// // 如果模板引擎是 freemarker
// String templatePath = "/templates/mapper.xml.ftl";
// // 如果模板引擎是 velocity
// // String templatePath = "/templates/mapper.xml.vm";
// // 自定义输出配置
// ArrayList<FileOutConfig> foclist = new ArrayList<>();
//
// foclist.add(new FileOutConfig() {
// @Override
// public String outputFile(TableInfo tableInfo) {
// // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
// return projectPath + "/src/main/resources/mapper/" + moduleName
// + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
// }
// });
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 如果是Entity类型,直接放过输出文件
if(fileType == FileType.ENTITY){
return true;
}
// 判断自定义文件夹是否需要创建
// checkDir("调用默认方法创建的目录,自定义目录用");
// if (fileType == FileType.MAPPER) {
// // 已经生成 mapper 文件判断存在,不想重新生成返回 false
// return !new File(filePath).exists();
// }
boolean exist = new File(filePath).exists();
//文件不存在或者全局配置的fileOverride为true才写文件
return !exist || configBuilder.getGlobalConfig().isFileOverride();
}
});
return cfg;
}
/**
* 模板配置
* @return
*/
static TemplateConfig initTemplateConfig(){
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setController("");
return templateConfig;
}
public static void main(String[] args) {
//代码生成器
AutoGenerator mpg = new AutoGenerator();
//配置策略
//1. 全局配置
mpg.setGlobalConfig(initGlobalConfig());
//2. 数据源
mpg.setDataSource(initDataSourceConfig());
//3. 包配置
PackageConfig pc = iniPackageConfig();
mpg.setPackageInfo(pc);
//4. 自定义配置 还没弄明白
mpg.setCfg(initInjectionConfig(pc.getModuleName()));
//5. 配置模板
mpg.setTemplate(initTemplateConfig());
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
//6. 策略配置
mpg.setStrategy(initStrategyConfig());
//执行
mpg.execute();
}
}
3.2 日志
这样,在控制台就可以输出SQL记录
#配置日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.3 MybitasPulsConfig
这个配置可以做很多事情,第一个事情,就是可以把**@MapperScan** 从项目的Application启动项拿走
@Configuration
@MapperScan("com.threeking.service.user.mapper")
public class MybatisPlusConfig {
}
4. 使用swagger2
4.1 旧时代额swagger
选择适用swagger做RESTful api文档接口
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
使用swagger-bootstrao-ui做交互界面
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.threeking.service.user"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swagger-bootstrap-ui RESTful APIs")
.description("swagger-bootstrap-ui")
.termsOfServiceUrl("http://localhost:6601/")
.contact(new Contact("ah","doc.html",""))
.version("1.0")
.build();
}
}
4.2 新时代的swagger,主要是swagger3.0之后的
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
<dependency>
@EnableOpenApi
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
访问:http://localhost:8080/swagger-ui/index.html 即可,方便简洁
如果想要继续使用swagger-bootstrap-ui
,在pom文件中添加
swagger-bootstrap-ui已经升级为knife4j,所以该方案废弃
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
访问:http://localhost:8080/doc.html 即可
4.3 使用knife4j做ui界面
knife4j官方文档
在pom.xml文件中添加引用,
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0</version>
</dependency>
其他不用变即可
5.处理Api响应返回值
spring官方给的是ResponseEntity,可以满足各种请求
但是实际项目开发中,底层服务,并不需要这么多处理,简化处理,我将其包装成一个基础类
APIBaseResponse.java
@Getter
@Setter
public class APIBaseResponse implements Serializable {
/**
* serialVersionUID
*/
private static final long serialVersionUID = 1L;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
/**
* 返回码
*/
private String code;
/**
* 返回信息
*/
private String msg;
}
一个继承类APIResponse.java,适配各种返回值
public class APIResponse<T> extends APIBaseResponse {
private static final long serialVersionUID = 1L;
private T content;
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
/**
* 请求成功返回的结构数据
* @param t t
* @return ResponseContentOne
*/
public static <T> APIResponse<T> successResp(T t){
APIResponse<T> resp = new APIResponse<T>();
resp.setCode("0");
resp.setMsg("success");
resp.setContent(t);
return resp;
}
public static <T> APIResponse<T> successResp(){
APIResponse<T> resp = new APIResponse<T>();
resp.setCode("0");
resp.setMsg("success");
return resp;
}
/**
* 请求失败返回的数据
* @param msg msg
* @return ResponseContentOne
*/
public static <T> APIResponse<T> errorResp(String msg){
return errorResp("1", msg);
}
/**
* 请求失败返回的数据
* @param msg msg
* @return ResponseContentOne
*/
public static <T> APIResponse<T> errorResp(String code, String msg){
APIResponse<T> resp = new APIResponse<>();
resp.setCode(code);
resp.setMsg(msg);
return resp;
}
public static <T> APIResponse<T> errorResp(BindingResult result){
APIResponse<T> resp = new APIResponse<T>();
resp.setCode("1");
StringBuilder sb = new StringBuilder("");
result.getAllErrors().stream()
.forEach(err -> {
sb.append(((FieldError) err).getField())
.append(":")
.append(err.getDefaultMessage())
.append(" | ");
});
String errMsg = sb.toString().trim();
errMsg = errMsg.substring(0, errMsg.lastIndexOf("|"));
resp.setMsg(errMsg);
return resp;
}
public static <T> APIResponse<T> errorRes(BindingResult bindingResult) {
APIResponse<T> resp = new APIResponse<>();
List<ObjectError> ls=bindingResult.getAllErrors();
resp.setCode("1");
resp.setMsg(ls.get(0).getDefaultMessage());
return resp;
}
}
使用方式
//使用方式
@PostMapping("/test")
public APIResponse test(){
return APIResponse.successResp();
return APIResponse.successResp("执行成功");
return APIResponse.errorResp("失败");
return APIResponse.errorResp("2","自定义code格式失败");
}
在项目过程中会有大量是使用,当然GitHub上还有其他大佬们的实现方式,按照自己的业务需求定制就可以了
6.入参校验
每个服务对外提供的都是一个个的API接口,那么每个接口的入参都需要特定的校验,如下:
@Getter
@Setter
public class PhoneDto {
private String phone;
private String verify;
}
这是一个手机验证码入参实体,我们需要在接受它的方法上对每个参数进行校验做处理, 参数校验有很多种方式
1. 原始方案,没一个做判断,做返回处理
@PostMapping("/phoneRegister")
public String testPhoneRegister(@RequestBody PhoneDto dto){
//参数判断
if(!checkparam(dto)){return "参数不对";}
// 其他处理
return "phoneRegister...";
}
这种方案,当然现在已经没有人选用
2. 借助javax.validation
实现,我们修改实体类
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter
@Setter
public class PhoneDto {
@Pattern(regexp = "1[\\d]{10}",message = "请输正确手机号")
private String phone;
@NotEmpty(message = "验证码不能为空")
private String verify;
}
修改接口,增加@Valid
注解
@PostMapping("/phoneRegister")
public String testPhoneRegister(@RequestBody @Valid PhoneDto dto){
// 其他处理
return "phoneRegister...";
}
请求后,接口会报这样的错误
虽然拦住了,但是并不是我们想要的结果,前端肯定不喜欢这样返回
3.使用BindingResult
方式返回错误信息
@PostMapping("/testPhoneRegister")
public String testPhoneRegister(@RequestBody @Valid PhoneDto dto, BindingResult result){
if (result.hasErrors())
{
List<ObjectError> ls=result.getAllErrors();
ls.forEach(e-> System.out.println(e.getDefaultMessage()));
//默认返回第一个错误,大家可以根据需求定制
return ls.get(0).getDefaultMessage();
}
return "phoneRegister...";
}
前端响应,接口回去我们的错误信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDCikbgn-1604633189686)(C:\Users\wanghui\AppData\Roaming\Typora\typora-user-images\image-20201105143114785.png)]
此时,已经基本满足我们的需求
但是,我们这么写,就太繁琐了,每个接口都要写一套,这时候我们应该想到Spring Boot AOP编程了
4.使用RestControllerAdvice
或者说ControllerAdvice
实现参数校验
spring framework web controller advice
@RestControllerAdvice
public class ParamValidControllerAdvice {
/**
* 拦截MethodArgumentNotValidException类型的错误
* 也就是我们定义Valid错误
* 也可以做一些其他的错误拦截器
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public APIResponse bindExceptionHandler(MethodArgumentNotValidException ex){
return APIResponse.errorResp(ex.getBindingResult());
}
}
前端拿到这样的结果,处理起来就比较方便了
7.统一全局异常类配置
前面校验引发的,我们后台异常怎么处理,一般情况下,都是写业务的时候,各种try-catch来处理我们的异常
但在实际开发中,我们并不想每个都处理,所以一直把异常throw出去,那么最外层用捕获的话,就回满屏幕都是try-catch
@PostMapping("/test1")
public void test1() {
try {
//to do something
} catch (Exception e) {
e.printStackTrace();
}
}
@PostMapping("/test2")
public void test2() {
try {
//to do something
} catch (Exception e) {
e.printStackTrace();
}
对我们开发者来说,我们只想考虑业务怎么写,所以我们一般都使用上面讲到的RestControllerAdvice
来做统一的错误日志处理
/**
* 全局异常处理类
*/
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {
/**
* 拦截MethodArgumentNotValidException类型的错误
* 也就是我们定义Valid错误
* 也可以做一些其他的错误拦截器
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public APIResponse<String> validExceptionHandler(MethodArgumentNotValidException ex){
return APIResponse.errorResp(ex.getBindingResult());
}
/**
* 处理运行异常
*/
@ExceptionHandler(RuntimeException.class)
public APIResponse<String> runtimeExceptionHandler(HttpServletRequest request, RuntimeException ex) {
log.error("", ex);
log.error("请求地址:" + request.getRequestURL());
log.error("请求参数: " + JSONUtils.toJSONString(request.getParameterMap()));
return APIResponse.errorResp(ex.getMessage());
}
/**
* 用来捕获404,400这种无法到达controller的错误
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
public APIResponse<String> exceptionHandler(Exception ex){
log.error("", ex);
if (ex instanceof NoHandlerFoundException) {
return APIResponse.errorResp("404",ex.getMessage());
} else {
return APIResponse.errorResp("500",ex.getMessage());
}
}
}
这个我们修改代码
@PostMapping("/test1")
public void test1() throws Exception{
//to do something
}
@PostMapping("/test2")
public void test2() throws Exception{
//to do something
}
这样是不是清爽很多了
8.使用Redis
缓存选择使用Redis,在pom文件中添加引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis:
host: 127.0.0.1
port: 6379
database: 6
jedis:
pool:
max-active: 20
max-idle: 100
min-idle: 1
max-wait: 1000ms
增加一个redis配置类
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
测试
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test(){
stringRedisTemplate.opsForValue().set("1332333333",
"123",
200,
TimeUnit.SECONDS);
redisTemplate.opsForValue().set("1332333333",
"123",
200,
TimeUnit.SECONDS);
}
转移到下一章:3.构建微服务体系