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
用法。