前言
2018年因为业务上需要选择了微服务架构,时间飞逝,转眼来到了2020年。当初的springboot版本也从1.5.x更新到了2.1.x。今天在这里想留下点springboot1.5.17版回忆,以纪念曾经的学习和方便工作上旧框架的使用。
微服务进入历史
我们来追溯一下web的发展历史:
曾几何时,web打天下的是SSH框架/SSM框架,静态页面的代码和操作数据库的逻辑都在同一个工程里面,架构大概是如图所示这样子的:
这种架构的特点:
- 开发:所有功能集中在一个项目中,前端代码,逻辑代码,数据库代码在同一个工程的不同目录下。
- 扩展:如果么一个模块出现性能瓶颈,这个模块可能是整个业务中的么个复杂逻辑功能,可能是数据库的部分查询语句无法更有效的优化,可能是么页面访问量大等等。扩展性能的主要方法是将相同代码部署到多台服务器上,然后在使用负载均衡的方法将流量分发到多个服务器上。
- 升级:项目中一个模块出现问题,那么整个项目都需要升级。
想想看这样的架构有什么问题?
如果么个模块出现性能问题,直接将出现性能问题的模块单独增加服务器资源即可,可现在的处理方案却是将所有模块都升级。
如果公司业务庞大,不同业务部分之前需要共享数据;例如处理日志数据的部门,要把日志数据给财务部门让他们统计客户的消费,要把日志数据给前端展示部门供他们展示客户的最近消费情况等等,以前的架构又如何处理呢?
微服务进入历史:微服务顾名思义即微小的服务,一个业务模块就可以是一个服务,换言之一个业务模块就是一个项目工程。他有何特点呢?来看如下:
- 灵活配置服务器的资源,出现性能瓶颈的服务可以分配更多的服务器资源。
- 通过HTTP互相通信,每一个服务最终都是一个可独立替换和独立升级的软件单元
- 单个服务出现问题不会影响其他业务服务的运行
- 根据业务提供不同的数据服务给不同的业务部门使用比较方便
- 如果公司有需要,在进行数据共享的时候,权限,日志,审查等等比单个应用更为健全和灵活
微服务架构看着很好,那它有没有什么缺陷呢?很明显,服务一旦很多,开发(搭建项目)、管理和升级这些服务就比较麻烦了。由此引入我们的springboot。
hello springboot
为了让我们更好的使用微服务,spring官方给了我们一站式解决方案:
我们借助于springboot
帮助我们实现快速开发,借助于spring cloud
进行部署便能解决微服务项目数量众多的问题。我们这里先来看下springboot的第一个案例,向浏览器发送"hello springboot":
1、创建maven工程,注意是jar工程
2、导入依赖的jar包
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.17.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3、编写主程序,启动springboot项目
@SpringBootApplication
public class SpringBootStudy {
public static void main(String[] args) {
// Spring应用启动起来
SpringApplication.run(SpringBootStudy.class,args);
}
}
4、编写相关controller
@RestController
public class HelloControlle {
@RequestMapping("/hello")
public String hello(){
return "hello springboot";
}
}
5、运行主程序,即main函数(因为是jar工程)
6、在浏览器中阅览效果:
http://localhost:8080/hello
如果此时你想打包项目,用命令行的方式运行,那么需要配置如下插件:
//如果想以java -jar xxx.jar方式运行jar工程,需要添加如下插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
配置文件和文件值注入
springboot的配置文件名默认是固定的,放在resources目录下:
application.properties或者application.yml
先来看下application.properties的用法
假如我的目标是让第一个案例的项目以端口8085启动,那么我们可以在文件中添加如下配置:
server.port=8085
再次访问该案例的服务就需要将端口修改为8085
http://localhost:8085/hello
其它可配置的参数请参考文档Part X. Appendices部分内容
上述案例我们体验了在application.properties
修改配置参数来配置系统环境。那接下来我们在体验一下将application.properties配置注入到Bean
对象中。
假如我们有如下Person对象:
/**
* @Component将Person注入到spring环境中
* @ConfigurationProperties指定配置文件中的那部分属性注入到该对象中,prefix表示注入以person开头的属性
* @author wuxf2
*
*/
@Component
@ConfigurationProperties(prefix="person")
public class Person {
private String name;
private Integer age;
private Boolean bool;
private List<Object> lists;
private String[] arr;
private Friend friend;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getBool() {
return bool;
}
public void setBool(Boolean bool) {
this.bool = bool;
}
public List<Object> getLists() {
return lists;
}
public void setLists(List<Object> lists) {
this.lists = lists;
}
public String[] getArr() {
return arr;
}
public void setArr(String[] arr) {
this.arr = arr;
}
public Friend getFriend() {
return friend;
}
public void setFriend(Friend friend) {
this.friend = friend;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", bool=" + bool + ", lists=" + lists + ", arr="
+ Arrays.toString(arr) + ", friend=" + friend + "]";
}
}
作为Person
对象属性的Friend
对象:
public class Friend {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Friend [name=" + name + ", age=" + age + "]";
}
}
我们想为上述对象注入application.properties文件中的属性,我们就可以这样写:
server.port=8085
person.name=wuxiaofeng33333333
person.age=28
person.bool=true
person.lists=list1,list2,list3
person.arr=arr1,arr2,arr3
person.friend.name=wangcai
person.friend.age=19
在再HelloControlle文件中添加访问该person的URL
:
@RestController
public class HelloControlle {
@Autowired
Person person;
@RequestMapping("/person")
public String hello(){
return person.toString();
}
}
最终的效果如下:
//浏览器输入
http://localhost:8085/person
//浏览器的访问结果
Person [name=wuxiaofeng33333333, age=28, bool=true, lists=[list1, list2, list3], arr=[arr1, arr2, arr3], friend=Friend [name=wangcai, age=19]]
另一个配置文件application.yml
yml
文件基本语法:
- k:(空格)v:表示一对键值对(空格必须有)
- 以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的
- 属性和值大小写敏感
值的写法:
1、字面量:普通的值(数字,字符串,布尔)
k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
"":加了双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思,例如:
name: "zhangsan \n lisi"
//输出;zhangsan 换行 lisi
'':单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据,例如:
name: ‘zhangsan \n lisi’
//输出;zhangsan \n lisi
2、对象、Map:属性和值,键值对
k: v:在下一行来写对象的属性和值的关系;
注意缩进对象还是k: v的方式
friend:
name: wangcai
age: 19
//行内写法
friend: {name: wangcai,age: 19}
3、数组(List、Set):
用- 值表示数组中的一个元素;
arr:
- arr1
- arr2
- arr3
//行内写法
arr: [arr1, arr2, arr3]
综合案例,上述案例application.properties
配置文件注入bean换成ym
l文件的写法就是:
server:
port: 8084
person:
name: wuxiaofeng2
age: 28
bool: true
lists:
- list1
- list2
- list3
arr:
- arr1
- arr2
- arr3
friend:
name: wangcai
age: 19
@Value字段注入
1、注入普通字符串
@Value("wangcai2")
private String name;
@Value注解用在成员变量name上,表明当前注入name的值为"wangcai2"
2、注入表达式
@Value("#{18 + 12}")
private Integer age;
@Value("#{1 == 1}")
private Boolean bool;
双引号中需要用到#{},可以进行加减法运算,也可以进行逻辑运算。
3、注入配置文件
@Value("${propertiesConfigValue}")
private String propertiesConfigValue;
双引号中为$符号而非#符号,{}中为配置文件中的key
。
上面的说明可能比较抽象,下面我们就来具体看一个案例:我们把一个用@Value各种方式配置的bean对象返回到浏览器。
创建要返回到浏览器的ValuePropertiesConfig对象
/**
* @Configuration作为配置文件
* @PropertySource指定其它位置的配置文件,encoding标识编码方式,以防乱码
*/
@Component
@Configuration
@PropertySource(value="classpath:value.properties", encoding = "UTF-8")
public class ValuePropertiesConfig {
@Value("wangcai2")
private String name;
@Value("#{18 + 12}")
private Integer age;
@Value("#{1 == 1}")
private Boolean bool;
@Value("${propertiesConfigValue}")
private String propertiesConfigValue;
@Override
public String toString() {
return "ValuePropertiesConfig [name=" + name + ", age=" + age + ", bool=" + bool + ", propertiesConfigValue="
+ propertiesConfigValue + "]";
}
}
value.properties配置文件内容
propertiesConfigValue="This come from Properties Config"
访问控制器代码
@RestController
public class HelloControlle {
@Autowired
ValuePropertiesConfig valuePropertiesConfig;
@RequestMapping("/valuePropertiesConfig")
public String getValuePropertiesConfig(){
return valuePropertiesConfig.toString();
}
}
浏览器访问@Value配置的bean对象数据
http://localhost:8086/valuePropertiesConfig
//结果为
ValuePropertiesConfig [name=wangcai2, age=30, bool=true, propertiesConfigValue="This come from Properties Config"]
如果觉得每次用浏览器访问@Value配置的bean对象数据很麻烦,我们可以改为用控制台
输出数据:
public static void main(String[] args) {
// Spring应用启动起来
//SpringApplication.run(SpringBootStudy.class,args);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValuePropertiesConfig.class);
ValuePropertiesConfig service = context.getBean(ValuePropertiesConfig.class);
System.out.println(service);
context.close();
}
Scope配置
Scope属性用于定义bean在容器中初始化的次数,singleton表示定义的bean为单例模式,prototype则适合多线程模式。
1)singleton场景模拟:
我们来新建一个Dog类,内容如下:
public class Dog {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
再来创建一个使用Dog类的singleton配置对象
/**
* @Configuration标识当前类是一个配置类,相当于spring的一个xml配置文件
* @Bean用在getDogSingleton方法上,表明当前方法返回一个Bean对象(Dog),然后将其交给 Spring 管理
* @Scope("singleton")可以完全省略,默认为singleton模式
*
*/
@Configuration
public class SingletonConfig {
@Bean
@Scope("singleton")
public Dog getDogSingleton() {
return new Dog();
}
}
最后我们来验证下结论:
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SingletonConfig.class);
Dog dog1 = context.getBean(Dog.class);
Dog dog2 = context.getBean(Dog.class);
System.out.println(dog1);
System.out.println(dog2);
dog1.setName("I am dog1");
System.out.println(dog2.getName());
context.close();
}
//输出效果:
com.study.bean.Dog@120b0058
com.study.bean.Dog@120b0058
I am dog1
可以看到,dog1和dog2的地址是一样的,并且修改了dog1的name值,dog2也跟着改变了。
2)prototype场景模拟
与上述案例相比,我们需要做的是创建一个使用Dog类的prototype配置对象
@Configuration
public class PrototypeConfig {
@Bean
@Scope("prototype")
public Dog getDogPrototype() {
return new Dog();
}
}
再来给变下验证结论的主函数:
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrototypeConfig.class);
Dog dog1 = context.getBean(Dog.class);
Dog dog2 = context.getBean(Dog.class);
System.out.println(dog1);
System.out.println(dog2);
dog1.setName("I am dog1");
System.out.println(dog2.getName());
context.close();
}
//输出效果:
com.study.bean.Dog@120b0058
com.study.bean.Dog@10439aa9
null
可以看到,dog1和dog2的地址不一样,并且改变dog1的name值并不会影响dog2。
为项目创建不同的环境变量
1、在配置文件application.yml
中区分环境变量
spring:
profiles:
active: dev
---
server:
port: 8086
person:
name: wuxiaofeng
age: 28
spring:
profiles: dev
---
server:
port: 8088
person:
name: "wuxiaofeng-prod"
age: 39
spring:
profiles: prod
application.yml文件中通过active
表示使用哪种环境,不同环境之间通过---进行分割,并且通过profiles
标识不同的环境名称。可以通过前面从配置文件中获取数据的方式进行验证,这里案例略。
2、在配置文件application.properties
中区分环境变量
使用application.properties方式区分环境变量需要配置多个properties配置文件,形如 application-${profile}.properties
,然后在application.properties
中指定调用的环境变量为${profile
}即可,来看个案例:
//application.properties文件
spring.profiles.active=prod
//application-prod.properties文件
server.port=8088
person.name="test-prod"
person.age=39
spring.profiles=prod
//application-dev.properties文件
server.port=8086
person.name=wuxiaofeng3333
person.age=28
spring.profiles=dev
我们创建了多个配置文件。通过spring.profiles.active=prod
标识我们调用application-prod.properties
配置文件。
日志框架
slf4j是一个日志框架的接口,log4j
和Logback
都实现了该接口(log4j需要借助slf4j-log4j12.jar适配)。logback在概念上与log4j非常相似,因为这两个项目都是由同一个开发人员创建的。如果您已经熟悉log4j,那么使用logback您会很快感到宾至如归。如果你喜欢log4j,你可能会喜欢logback。与log4j相比,Logback带来了许多大大小小的改进。详情改进太多,具体可以参考官方文档
使用方式:只需要在resources下创建logback-spring.xml文件进行配置即可(springboot官方推荐优先使用带有-spring的文件名作为定义的日志配置,这样可以为它添加一些Spring Boot特有的配置项)
开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法;给系统里面导入slf4j的jar和 logback的实现jar
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
日志的默认格式输出如下:
2014-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52
2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms
2014-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2014-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
上述输出的日志信息,从左往右含义解释如下:
日期时间:精确到毫秒
日志级别:ERROR,WARN,INFO,DEBUG or TRACE
进程:id
分割符:用于区分实际日志消息的开头
线程名:括在方括号中(可以为控制台输出截断)
日志名字:通常是源类名
日志信息
看一个企业级的日志配置
首先看下application.properties
的配置:
//指定操作系统的路径,其它配置项logging.file等可以在logback-spring.xml指定更合适
// windows操作系统
logging.path=D:\\mars\\springBootStudy001\\test\\log
// linux操作系统
logging.path=/usr/local/wss_management_service/var/log
再来看下logback-spring.xml
配置:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="consoleApp" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</layout>
</appender>
<appender name="fileInfoApp" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</encoder>
<!-- 滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 路径 -->
<fileNamePattern>${LOG_PATH}/app.info.%d.log</fileNamePattern>
</rollingPolicy>
</appender>
<appender name="fileErrorApp" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>
%date{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger{56}.%method:%L -%msg%n
</pattern>
</encoder>
<!-- 设置滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 路径 -->
<fileNamePattern>${LOG_PATH}/app.err.%d.log</fileNamePattern>
<!-- 控制保留的归档文件的最大数量,超出数量就删除旧文件,假设设置每个月滚动,且<maxHistory> 是1,则只保存最近1个月的文件,删除之前的旧文件 -->
<!-- <MaxHistory>1</MaxHistory> -->
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="consoleApp"/>
<appender-ref ref="fileInfoApp"/>
<appender-ref ref="fileErrorApp"/>
</root>
</configuration>
上述配置中:
-
${LOG_PATH}
获取的就是application.propertie
s配置文件中的logging.path
属性值 - 日志级别默认设置的是
INFO
,可以将level
设置成debug
来更改日志级别(日志太多,不建议) - 其它配置项详情见logback官方文档
结束语
这期先回顾一下springboot中常用的配置,下期我们继续回顾springboot-web
用法。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。