2018年因为业务上需要选择了微服务架构,时间飞逝,转眼来到了2020年。当初的springboot版本也从1.5.x更新到了2.1.x。今天在这里想留下点springboot1.5.17版回忆,以纪念曾经的学习和方便工作上旧框架的使用。
我们来追溯一下web的发展历史:
曾几何时,web打天下的是SSH框架/SSM框架,静态页面的代码和操作数据库的逻辑都在同一个工程里面,架构大概是如图所示这样子的:
这种架构的特点:
想想看这样的架构有什么问题?
如果么个模块出现性能问题,直接将出现性能问题的模块单独增加服务器资源即可,可现在的处理方案却是将所有模块都升级。
如果公司业务庞大,不同业务部分之前需要共享数据;例如处理日志数据的部门,要把日志数据给财务部门让他们统计客户的消费,要把日志数据给前端展示部门供他们展示客户的最近消费情况等等,以前的架构又如何处理呢?
微服务进入历史:微服务顾名思义即微小的服务,一个业务模块就可以是一个服务,换言之一个业务模块就是一个项目工程。他有何特点呢?来看如下:
微服务架构看着很好,那它有没有什么缺陷呢?很明显,服务一旦很多,开发(搭建项目)、管理和升级这些服务就比较麻烦了。由此引入我们的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
假如我的目标是让第一个案例的项目以端口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]]
yml 文件基本语法:
值的写法:
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
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属性用于定义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 来更改日志级别(日志太多,不建议) 这期先回顾一下springboot中常用的配置,下期我们继续回顾 springboot-web 用法。