微服务开发
本次最佳实践主要对于传统开发的劣势及存在问题进行分析,及描述微服务开发带来的优势,指导传统服务开发到微服务的改造。也将详细描述微服务开发设计过程,对不同的层面进行字面和代码示例方式指导微服务开发。
最佳实践背景
对于软件来说,有一套开发规约绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,推行相对标准化,以一种普遍认可的方式做事。
在过去的几年里,大多数团队都会使用微服务架构来构建产品,他们使用微服务架构的意图都是正确的:更快的开发速度、更好的可扩展性、更小的独立团队、独立的部署、使用合适的技术来完成工作等等。但大多数时候,团队在使用微服务时都很不顺利。
由于历史隔阂与业务风格差异,导致工程结构差别很大,代码风格迥异,规范不一,沟通成本大,合作效率低,维护成本高。因此,应该需要专业化的迭代式、集约式发展,而不是动辄重复造论,真正专业化的团队一定会有统一的开发规约,这代表效率、共鸣、情怀和可持续。
最佳实践价值
本次最佳实践主要目标:
码出高效:提升代码开发效率,提升沟通效率和研发效能。
码出质量:防患于未然,提升质量意识和系统可维护性,降低故障率。
码出情怀:工匠精神,追求极致的卓越精神,打磨精品代码。
微服务项目开发分析
传统应用程序成为了一个庞大、复杂的单体,开发组织可能会陷入了一个痛苦的境地,敏捷开发和交付的任何一次尝试都将原地徘徊。最终正确修复bug和实现新功能变得非常困难而耗时。此外,这种趋势就像是往下的螺旋。如果基本代码都令人难以理解,那么改变也不会变得正确,最终得到的将是一个巨大且不可思议的大泥球。
目前传统IT企业拥有大量的单体应用,不能快捷方便的管理应用,如果服务器停机,由于需要手工流程所以需要较长的时间来恢复,很难通过增加新的实例来进行横向扩展。而新的应用开始变得越来灵活,开发越来越迅捷,不停的敏捷迭代适应快速变化的市场,同时应用本身必须要保证服务的稳定性,可扩展性,增强用户体验。
为了适应新形势下的发展潮流,单个应用微服务架构改造,让系统的应用开发、部署、运维模式发生改变,最终应当实现在有限信息资源下应用建设效率提升、应用系统性能提升、应用架构水平整体提升,从而完成企业转型的关键战略升级。
微服务项目架构设计
微服务开发设计
应用分层:规范构建项目,便于后续开发及维护。
父子项目抽取:统一管理依赖,便于维护。
公共子模块抽取:共性代码提取并提供规范使用,便于后续运维和开发。
代码生成器:通用三层代码生成,方便快速开发项目。
日志封装:使用AOP统一日志处理,解耦控制层。
异常处理:简化代码,避免异常的遗漏及断言判断封装。
通用接口返回JSON封装:统一规范返回数据结构。
Model工厂模式设计:统一规范实体创建。
公共配置文件设计:简化配置,方便管理配置文件。
接口健康检查:提供服务健康检查接口。
动态多数据源设计:动态数据源,支持多主多从、纯粹多库、混合配置。
Swagger2接口文档:统一接口管理的动态界面化框架。
标准化RESTful风格接口:一套成熟的互联网API设计理论,标准化设计接口。
分布式锁设计:解决定时任务在多实例环境下重复执行以及表单重复提交。
Logback日志设计:统一日志打印及格式。
应用分层
应用分层须遵循:
方便后续代码进行维护扩展。
分层的效果让整个团队接受。
各个层职责边界清晰。
应用分层图:
开发接口层:可直接封装Service方法暴露成RPC接口;通过Web封装成HTTP接口;进行网关安全控制、流量控制等。
终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。
请求处理层(Web层):主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
业务逻辑层(Service层):相对具体的业务逻辑服务层。
通用处理侧(Manager层):通用业务处理层,它有如下特征:
对第三方平台封装的层,预处理返回结果及转化异常信息。
对Service层通用能力的下沉,如缓存方案、中间件通用处理。
与DAO层交互,对多个DAO的组合复用。
数据持久层(DAO层):数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。
外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。
父子项目抽取
在微服务开发过程中,随业务的不断扩展微服务个数会随之增加。大规模的微服务组成了一个复杂的项目,而多模块开发在每个模块都对应着一个pom.xml。它们之间通过继承和聚合而相互关联,如不加以管理,随之依赖会杂乱冗余,父级项目目的就在于此。
父子项目中pom.xml编写规范
Properties:统一管理所有JAR包版本,通过定义全局变量,在POM中依赖包通过${property_name}的形式引用变量的值。下面提供代码以供参考:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.SR1</spring-cloud.version> </properties>
Dependency Management:所有JAR包依赖管理,只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom。另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。下面提供代码以供参考:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.0.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Plugin Management:打包插件管理,与dependencyManagement非常类似,PluginManagement下的plugins下的plugin则仅仅是一种声明,子项目中可以PluginManagement下的plugin进行信息的选择、继承、覆盖等。下面提供代码以供参考:
<pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.2.0.RELEASE</version> </plugin> </plugins> </pluginManagement>
公共子模块抽取
微服务的思想:有多个服务,把一个项目拆分成多个独立的服务,多个服务是独立运行的,每个服务占用独立的进程。 所以能拆即拆,模块化开发的思想设计。
在实际开发过程中,服务会分为很多个模块,但是有些实体类或接口会在很多模块使用,这样可以将其单独放在一个模块中,其他模块要使用的时候,直接调用,可以大幅简化了=开发配置,以及提高开发效率。
公共子模块
共性代码提取并提供规范使用,便于后续运维和开发,示例图如下:
在其它需要使用,在pom.xml的dependencies下引入公共模块即可:
<dependency>
<groupId>com.poc.springcloud</groupId>
<artifactId>microservicecloud-api</artifactId>
<version>${project.version}</version>
</dependency>
代码生成器
在平时开发过程中,常有机械的、重复的代码编写,因此自动完成这些重复性内容来节省时间,节省代码,保持理性。代码生成器用来生成有规律的代码,如dao、modelEntity、mapper、mapper.xml等。
代码生成器示例代码如下:
public class CodeGenerator {
public static void generator(String moduleName, String author, String packageParent, String tablePrefix,
String[] include, String driverName, String username, String password, String url) {
//代码生成器
AutoGenerator mpg = new AutoGenerator().setGlobalConfig();
}
}
public class CodeGeneratorTest {
public static void main(String[] args) {
CodeGenerator.generator("micro-demo", "lhh", "com.micro.demo", "sin_", new String[]{"sin_test"}, "oracle.jdbc.OracleDriver", "****", "****",
"jdbc:oracle:thin****");
}
}
日志封装
在开发过程中,随处可见日志的处理,为提高开发效率及规范日志处理,非常有必要提供全局日志处理。
不建议使用System.out,因为大量的使用会增加资源的消耗。使用System.out是在当前线程执行的,写入文件也是写入完毕之后才继续执行下面的程序。而使用Log工具不但可以控制日志是否输出,怎么输出,它的处理机制也是通知写日志,继续执行后面的代码不必等日志写完。
下面介绍微服务开发过程中最常用日志打印方式,使用slf4j+logback实现日志记录:
在已构建好的Maven工程中的pom.xml引入下面依赖。
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
在Maven工程resources目录下新建logback-spring.xml文件。
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <contextName>logs</contextName> <!-- 日志位置 --> <property name="log.path" value="log" /> <!-- 日志保留时长 --> <property name="log.maxHistory" value="15" /> <property name="log.colorPattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %yellow(%thread) %green(%logger) %msg%n"/> <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5level %thread %logger %msg%n"/> <!--输出到控制台--> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${log.colorPattern}</pattern> </encoder> </appender> <!--输出到文件--> <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern> <MaxHistory>${log.maxHistory}</MaxHistory> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/error/error.%d{yyyy-MM-dd}.log</fileNamePattern> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 日志类型为debug时,输出到控制台 --> <root level="debug"> <appender-ref ref="console" /> </root> <!-- 日志类型为info时,输出到配置好的文件 --> <root level="info"> <appender-ref ref="file_info" /> <appender-ref ref="file_error" /> </root> </configuration>
记录日志,使用slf4j注解,然后调用log方法就可以直接生成日志文件。
package springboot.demo.log.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Administrator on 2019/10/26. * @version 1.0 */ @Slf4j @RestController public class TestController { @GetMapping("/index") public void index() { log.info("我记录日志了"); } }
异常处理
在项目的开发中,不管是对底层的数据库操作、业务层的处理过程、控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。
解耦异常处理,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。
统一异常处理示例代码如下:
public class SinopeHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
private static final ModelAndView MODEL_VIEW_INSTANCE = new ModelAndView();
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex instanceof ApiException) {
handleApi((ApiException) ex, request, response);
} else if (ex instanceof HttpRequestMethodNotSupportException) {
handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportException) ex, request, response);
} else if (ex instanceof HttpMediaTypeNotSupportException) {
handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportException) ex, request, response);
} else if (ex instanceof HttpMediaTypeNotAcceptableException) {
handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response);
} else if (ex instanceof MissingPathVariableException) {
handleMissingPathVariable((MissingPathVariableException) ex, request, response);
} else if (ex instanceof MissingServletRequestParameterException) {
handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request, response);
} else if (ex instanceof ServletRequestBindingException) {
handleServletRequestBindingException((ServletRequestBindingException) ex, request, response);
} else if (ex instanceof ConversionNotSupportedException) {
handleConversionNotSupported((ConversionNotSupportedException) ex, request, response);
}
}
}
public void handleApi(ApiException ex, HttpServletRequest request, HttpServletResponse response) {
ResponseUtil.sendFail(request, response, ex.getErrorCode());
}
通用接口返回JSON封装
在项目开发中,如果将每个接口都要封装的方式,直接封装成统一格式,就可以避免不同人开发,返回格式不统一的问题,而灵活快速又易懂的返回数据是非常关键的。
JSON封装示例代码如下:
public class SuccessResponse<T> extends ApiResponse<T> {
private Integer status;
private T result;
}
public class FailedResponse extends ApiResponse {
private Integer status;
private String error;
private String msg;
private String exception;
private LocalDateTime time;
}
返回JSON报文示例:
public static ApiResponse<Void> success(HttpServletResponse response, HttpStatus status) {
response.setStatus(status.value());
return SuccessResponse.builder().status(status.value()).build();
}
public static <T> FailedResponse failure(ErrorCode errorCode, Exception exception) {
return ResponseUtil.exception(FailedResponse.builder().msg(errorCode.getMsg()), exception).error(errorCode.getError())
.show(errorCode.isShow()).time(LocalDateTime.now()).status(errorCode.getHttpCode).build();
}
GetMapping("/{id}")
public ApiResponse<User> get(@PathVariable("id") Long id) {
User user = userService.getById(id);
ApiAssert.notNull(ErrorCodeEnum.USER_NOT_FOUNT, user);
return success(user);
}
客户端超时配置
API形式配置HSF服务,配置HSFApiConsumerBean的clientTimeout属性,单位是ms,我们把接口的超时配置为1000ms,方法queryOrder配置为100ms,代码如下:
HSFApiConsumerBean consumerBean = new HSFApiConsumerBean(); //接口级别超时配置 consumerBean.setClientTimeout(1000); //xxx MethodSpecial methodSpecial = new MethodSpecial(); methodSpecial.setMethodName("queryOrder"); //方法级别超时配置,优先于接口超时配置 methodSpecial.setClientTimeout(100); consumerBean.setMethodSpecials(new MethodSpecial[]{methodSpecial});
Spring配置HSF服务,上述例子中的API配置等同于如下XML配置:
<bean id="CallHelloWorld" class="com.taobao.hsf.app.spring.util.HSFSpringConsumerBean"> ... <property name="clientTimeout" value="1000" /> <property name="methodSpecials"> <list> <bean class="com.taobao.hsf.model.metadata.MethodSpecial"> <property name="methodName" value="queryOrder" /> <property name="clientTimeout" value="100" /> </bean> </list> </property> ... </bean>
服务端超时配置
API形式配置HSF服务,配置HSFApiProviderBean的clientTimeout属性,单位是ms,代码如下:
HSFApiProviderBean providerBean = new HSFApiProviderBean(); //接口级别超时配置 providerBean.setClientTimeout(1000); //xxx MethodSpecial methodSpecial = new MethodSpecial(); methodSpecial.setMethodName("queryOrder"); //方法级别超时配置,优先于接口超时配置 methodSpecial.setClientTimeout(100); providerBean.setMethodSpecials(new MethodSpecial[]{methodSpecial});
Spring配置HSF服务,上述例子中的API配置等同于如下XML配置:
<bean class="com.taobao.hsf.app.spring.util.HSFSpringProviderBean" init-method="init"> ... <property name="clientTimeout" value="1000" /> <property name="methodSpecials"> <list> <bean class="com.taobao.hsf.model.metadata.MethodSpecial"> <property name="methodName" value="queryOrder" /> <property name="clientTimeout" value="2000" /> </bean> </list> </property> ... </bean>
Model工厂模式设计
创建使用工厂模式的目的在于,在每新创建一个实体时,就要去修改匹配的构造函数,而修改代码时非常谨慎的,为避免准备构造方法的参数以及new对象(new对象其实也是一种硬编码的表现),所以就需要引入工厂方法模式。
示例代码如下:
public class EntityFactoryBean implements FactoryBean<BaseEntity> {
@Nullable
private static BaseEntity baseEntity;
public static BaseEntity getInstance(Class<? extends BaseEntity> clazz) {
try {
baseEntity = (BaseEntity)Class.forName(clazz.getName()).newInstance();
} catch(Exception e) {
log.error("EntityFactoryBean create Entity failed:" + e.getMessage(), e);
e.printStackTrace();
}
return baseEntity;
}
}
@GetMapping("/{id}")
public ApiResponse<Void> update(@PathVariable("id") Long id, @RequestBody @Validated(UserParm.Update.class) UserParm userPARM) {
User user = (User)EntityFactoryBean.getInstance(User.class);
BeanUtils.copyProperties(userPARM, user);
user.setUserId(id);
userService.updateById(user);
return success();
}
公共配置文件设计
把公共的配置文件独立出来,方便统一管理,项目部署,避免在使用时因为配置文件杂乱无章而导致的错误引用。
公共配置文件application-common.yaml抽取:
#mybatis-plus配置:
mybatis-plus:
mapper-locations: classpath:dao/*Mapper.xml
global-config:
banner: false
supper-mapper-class: com.micro.common.base.BaseMapper
spring:
profiles:
include: common
active:
接口健康检查
在每一个服务中都应该提供一个健康检查接口,此接口须执行简单的数据查询,用来提供给Kubernetes做自动服务健康检查。
示例代码如下:
@RestController
@RequestMapping(value = "/health_check", produces = "applicatioin/json")
public class HealthCheckController extends BaseController {
@Autowired
private IUserService userService;
public ApiResponse<User> get(@PathVariable("id") Long id) {
User user = userService.getById(id);
ApiAssert.notNull(ErrorCodeEnum.USER_NOT_ROUNT, user);
return success(user);
}
}
动态多数据源设计
随着业务的发展,数据库压力的增大,如何分割数据库的读写压力时我们需要考虑的问题,而能够动态的切换数据源就是我们的首要目标。
在原有项目里使用多数据源时显得非常冗余,为此封装多数据源的使用非常关键,这里使用注解和面向切面来实现动态的数据源切换。
注:在使用注解切换数据源时,把@DS用到Mapper接口层上。
项目引入JAR依赖:
<dependency>
<groupId>com.baomidu</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</denpendency>
spring:
datasorce:
dynamic:
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
max-pool-size: 20
primary: sinopec
datasorce:
sinopec:
username: ****
password: ****
driver-class-name: com.mysql.cj.jdbc.Driver
url: ****
hypergraph:
username: ****
password: ****
url: ****
driver-class-name: oracle.jdbc.OracleDriver
@DS("hypergraph")
public interface TestMapper extends BaseMapper<Test> {
}
Swagger2接口文档
在各个细分组织人员共同完成项目,共同完成产品的全周期工作。如何进行组织架构内的有效高效沟通就显得尤其重要。其中,如何构建一份合理高效的接口文档更显重要。
而Swagger的出现可以完美解决以上传统接口管理方式存在的痛点,动态界面化接口管理。
项目引入JAR包依赖:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
获取Swagger ApiInfo:
@Configuration
@ComponentScan(basePackages = { "com.aliware.edas.controller" })
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket swaggerSpringfoxDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.paths(Predicates.or( //这里添加你需要展示的接口
PathSelectors.ant("/poc/**")
)
)
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("POC功能API接口测试文档")
.description("description")
.contact("liuhuihui")
.version("1.0")
.build();
}
}
代码中通过注解使用Swagger功能:
@RestController
@RequestMapping(value = "/poc")
@RefreshScope
@Api(description = "Spring Cloud Alibaba-POC功能测试接口")
public class ConsumerController {
@Value("${useLocalCache:false}")
private String useLocalCache;
InetAddress ip;
@ApiOperation(value = "全局参数配置")
@RequestMapping(value = "/echo-acm", method = RequestMethod.GET)
public String echoAcm() {
return "配置文件中的value值:" + useLocalCache + "\r\n";
}
@ApiOperation(value = "获取版本信息")
@RequestMapping(value = "/echo-version", method = RequestMethod.GET)
public String getVersion() throws UnknownHostException {
ip = InetAddress.getLocalHost();
String localName=ip.getHostName();
String localIp = ip.getHostAddress();
System.out.println("主机名称:" + localName + "ip地址:" + localIp);
return "This version is V1" + "\r\n";
}
}
标准化RESTful风格接口
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷。因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
常用HTTP动词及使用例子:
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
分布式锁设计
现在大多数应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。保证分布式部署的应用集群具有以下特点:
同一个方法在同一时间只能被一台机器的一个线程执行。
使用重入锁(避免死锁)。
高可用的获取锁和释放锁功能。
获取锁和释放锁的性能要好。
因此采用高可用的Redis来实现分布式锁是可行的。Demo的分布式锁主要是防此多实例定时任务重复执行及表单重复提交。
示例代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public interface CacheLock {
String prefix() default "";
int expire() default 5;
TimeUnit timeUnit() default TimeUnit.SECONDS: String delimiter() default ":";
}
@Target(ElementType.PARAMETER, ElementType.METHOD, ElementType.FIBLD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public interface CacheParm {
String name() default "";
}
@Aspect
@Configuration
public class LockMethodAspect {
private RedisLockHelper redisLockHelper;
@Around("execution(public * *(..) && @annotation(com.micro.demo.common.annotation.CacheLock)"))
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature)pjp.getSignature();
Method method = signature.getMethod();
CacheLock lock = method.getAnnotation(CacheLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key don't null...");
}
final String lockKey = AbstractLockKeyGenerator.getLock(pjp);
String value = UUID.randomUUID().toString();
try {
final boolean = redisLockHelper.lock(lockKey.value, lock.expire(), lock.timeUnit());
if (!success) {
throw new RuntimeException("重复提交");
}
try {
return pjp.proceed();
} catch(Throwable thowable) {
throw new RuntimeException("系统异常");
}
} finally {
redisLockHelper.unlock(lockKey, value);
}
}
}
Logback日志设计
相比其它日志框架logback配置更加简单和效率,项目中日志记录的完整性能够帮助我们更好的分析,解决线上出现的各种问题,方便问题的快速定位。项目中用到日志的几个场景:记录后台的SQL输出,记录主要业务的执行,报警系统需要对不同的日志进行监控,需要做日志的分离等。
日志输出配置文件代码如下:
<?xml version="1.0
" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<property name="LOG_FILE_NAME_PATTERN" value="logs/auth.%d{yyyy-MM-dd}.%i.log"/>
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%c){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<property name="FILE_LOG_PATTERN"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--输出到文件-->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_NAME_PATTERN}</fileNamePattern>
<!-- 日志保留天数 -->
<maxHistory>366</maxHistory>
<!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
<totalSizeCap>2GB</totalSizeCap>
<!-- 每个日志文件的最大值 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- (多环境配置日志级别)根据不同的环境设置不同的日志输出级别 -->
<springProfile name="default,local">
<root level="info">
<appender-ref ref="console"/>
</root>
<logger name="com.zhl" level="debug"/>
</springProfile>
<springProfile name="dev,test">
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
<logger name="com.zhl" level="debug"/>
</springProfile>
<springProfile name="product,pre">
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
<logger name="com.zhl" level="debug"/>
</springProfile>
</configuration>
不同日志信息输出到不同文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称-->
<property name="appName" value="zhlrm-ppt-service"/>
<!-- 日志的存放目录-->
<!-- debug-->
<property name="DEBUG_LOG_FILE_NAME_PATTERN" value="logs/${appName}-debug.%d{yyyy-MM-dd}.%i.log"/>
<property name="INFO_LOG_FILE_NAME_PATTERN" value="logs/${appName}-info.%d{yyyy-MM-dd}.%i.log"/>
<property name="WARN_LOG_FILE_NAME_PATTERN" value="errlogs/${appName}-warn.%d{yyyy-MM-dd}.%i.log"/>
<property name="ERROR_LOG_FILE_NAME_PATTERN" value="errlogs/${appName}-error.%d{yyyy-MM-dd}.%i.log"/>
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%c){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<property name="FILE_LOG_PATTERN"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--输出到DEBUG文件-->
<appender name="debug_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${DEBUG_LOG_FILE_NAME_PATTERN}</fileNamePattern>
<!-- 日志保留天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
<totalSizeCap>2GB</totalSizeCap>
<!-- 每个日志文件的最大值 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<!-- 此日志文件只记录debug级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debug</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--输出到INFO文件-->
<appender name="info_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${INFO_LOG_FILE_NAME_PATTERN}</fileNamePattern>
<!-- 日志保留天数 -->
<maxHistory>7</maxHistory>
<!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
<totalSizeCap>1GB</totalSizeCap>
<!-- 每个日志文件的最大值 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--输出到WARN文件-->
<appender name="warn_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${WARN_LOG_FILE_NAME_PATTERN}</fileNamePattern>
<!-- 日志保留天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
<totalSizeCap>1GB</totalSizeCap>
<!-- 每个日志文件的最大值 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--输出到ERROR文件-->
<appender name="error_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${ERROR_LOG_FILE_NAME_PATTERN}</fileNamePattern>
<!-- 日志保留天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
<totalSizeCap>1GB</totalSizeCap>
<!-- 每个日志文件的最大值 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<!-- 此日志文件只记录error级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- region 根据不同的环境设置不同的日志输出级别 -->
<springProfile name="default,local,dev">
<root level="info">
<appender-ref ref="console"/>
</root>
<logger name="com.zhl.rm" level="debug"/>
</springProfile>
<springProfile name="prod,pre,test">
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="debug_file"/>
<appender-ref ref="info_file"/>
<appender-ref ref="warn_file"/>
<appender-ref ref="error_file"/>
</root>
<logger name="com.zhl.rm" level="debug"/>
</springProfile>
<!-- endregion -->
</configuration>
微服务开发选用SpringBoot
Spring Boot 是一个真正的游戏改变者。Spring Boot是一个构建在Spring 框架顶部的项目,它提供了一种更简单、更快捷的方式来设置、配置和运行简单的基于Web的应用程序。
在过去Spring框架中,我们需要为应用配置所有的内容,会有许多配置文件,例如XML或元注释,这是Spring Boot解决的主要问题之一,基本无需XML配置了,都使用@注释。
Spring boot巧妙地根据我们选择的依赖配置,可以自动启动我们想要的所有功能,并且只需单击一下即可启动应用程序。此外,它还简化了应用程序的部署过程。
Spring Boot还有很多功能特点:
自动配置:在启动时检测到框架将自动配置,如JDBC。
Starter:帮助项目启动,自动添加启动项目依赖项。
实现微服务:提供REST风格API暴露微服务与客户端交互。
自定义配置:通过配置文件来实现各个组件的基本配置。
模块化:一个Spring Boot应用是一个微服务,相当于一个模块,多模块开发后使用Spring Cloud实现动态访问与监控再使用分布式运行,累计扩大规模。
独立打包:一个Spring Boot应用独立设计,生产级质量的应用,通过简单的配置和部署嵌入式Web服务器。
内嵌服务器:默认是Tomcat,可支持Jetty、undertow。
Spring Cloud基础:Spring Cloud是实现分布式微服务的组件,其基础就是多个Spring Boot 微服务,如服务发现就是嵌入了Eureka 组件的Spring Boot。