源码链接:https://pan.baidu.com/s/1q9s23u4UwGnqG0dJFIvXaA
目录
案例的功能是统计网站独立IP访问次数的功能,并将访问信息在后台持续输出。整体功能是在后台每默认5秒输出一次监控信息(格式:IP+访问次数),如果使用者自己配置了相关的更新周期时间那么就使用使用者配置的更新时间 ,当用户访问网站时,对用户的访问行为进行统计。
例如:张三访问网站功能15次,IP地址:192.168.0.135,李四访问网站功能20次,IP地址:61.129.65.248。那么在网站后台就输出如下监控信息,此信息默认每5秒刷新一次。
IP访问监控
+-----ip-address-----+--num--+
| 192.168.0.135 | 15 |
| 61.129.65.248 | 20 |
+--------------------+-------+
在进行具体制作之前,先对功能做具体的分析:
-
数据记录在什么位置
最终记录的数据是一个字符串(IP地址)对应一个数字(访问次数),此处可以选择的数据存储模型可以使用java提供的map模型,也就是key-value的键值对模型,或者具有key-value键值对模型的存储技术,例如redis技术。本案例使用map作为实现方案,有兴趣的小伙伴可以使用redis作为解决方案。
-
统计功能运行位置,因为每次web请求都需要进行统计,因此使用拦截器会是比较好的方案,本案例使用拦截器来实现。不过在制作初期,先使用调用的形式进行测试,等功能完成了,再改成拦截器的实现方案。
-
为了提升统计数据展示的灵活度,为统计功能添加配置项。输出频度,输出的数据格式,统计数据的显示模式均可以通过配置实现调整。
-
输出频度,默认5秒
-
数据特征:累计数据 / 阶段数据,默认累计数据
-
输出格式:详细模式 / 极简模式
-
在下面的制作中,分成若干个步骤实现。先完成最基本的统计功能的制作,然后开发出统计报表,接下来把所有的配置都设置好,最后将拦截器功能实现,整体功能就做完了。
1.IP计数业务功能开发
本功能最终要实现的效果是在现有的项目中导入一个starter,对应的功能就添加上了,删除掉对应的starter,功能就消失了,要求功能要与原始项目完全解耦。因此需要开发一个独立的模块,制作对应功能。
步骤一:创建全新的模块,定义业务功能类
功能类的制作并不复杂,定义一个业务类,声明一个Map对象,用于记录ip访问次数,key是ip地址,value是访问次数
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
}
有些小伙伴可能会有疑问,不设置成静态的,如何在每次请求时进行数据共享呢?记得,当前类加载成bean以后是一个单例对象,对象都是单例的,哪里存在多个对象共享变量的问题。
步骤二:制作统计功能
制作统计操作对应的方法,每次访问后对应ip的记录次数+1。需要分情况处理,如果当前没有对应ip的数据,新增一条数据,否则就修改对应key的值+1即可
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问的次数
//1.获取当前操作的ip
String ip = httpServletRequest.getRemoteAddr();
//测试一下这个ip地址存不存在,就是有没有拿到这个ip地址
//System.out.println("---------" + ip);
//2.根据IP地址从map中取值,并递增
Integer count = ipCountMap.get(ip);
if ( count == null){
//第一次访问的,自己存在map中去
ipCountMap.put(ip,1);
}else {
ipCountMap.put(ip,count+1);
}
}
}
因为当前功能最终导入到其他项目中进行,而导入当前功能的项目是一个web项目,可以从容器中直接获取请求对象,因此获取IP地址的操作可以通过自动装配得到请求对象,然后获取对应的访问IP地址。
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
//当前的request对象的注入工作由使用当前starter的工程提供自动装配
private HttpServletRequest httpServletRequest;
public void count(){
//每次调用当前操作,就记录当前访问的IP,然后累加访问的次数
//1.获取当前操作的ip
String ip = httpServletRequest.getRemoteAddr();
//测试一下这个ip地址存不存在,就是有没有拿到这个ip地址
//System.out.println("---------" + ip);
//2.根据IP地址从map中取值,并递增
Integer count = ipCountMap.get(ip);
if ( count == null){
//第一次访问的,自己存在map中去
ipCountMap.put(ip,1);
}else {
ipCountMap.put(ip,count+1);
}
}
}
步骤三:定义自动配置类
我们需要做到的效果是导入当前模块即开启此功能,因此使用自动配置实现功能的自动装载,需要开发自动配置类在启动项目时加载当前功能。
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
或者是使用导入文件来配置:
@Import(IpCountService.class)
public class IpAutoConfiguration {
}
自动配置类需要在spring.factories文件中做配置方可自动运行。这个配置文件放在下面位置:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.itcast.autoconfig.IpAutoConfiguration
步骤四:在原始项目中模拟调用,测试功能
原始调用项目中导入当前开发的starter
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>ip_spring_boot_starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
推荐选择调用方便的功能做测试,推荐使用分页操作,当然也可以换其他功能位置进行测试。
测试的小项目的源码地址:BootDemo: 这是一个spring Boot 项目的demo,持久层使用的mybatis-plushttps://gitee.com/ljmandjh/Boot.git
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private IpCountService ipCountService;
@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
ipCountService.count();
IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
if( currentPage > page.getPages()){
page = bookService.getPage((int)page.getPages(), pageSize,book);
}
return new R(true, page);
}
}
温馨提示
由于当前制作的功能需要在对应的调用位置进行坐标导入,因此必须保障仓库中具有当前开发的功能,所以每次原始代码修改后,需要重新编译并安装到仓库中。为防止问题出现,建议每次安装之前先clean然后install,保障资源进行了更新。切记切记!!
当前效果
每次调用分页操作后,可以在控制台输出当前访问的IP地址,此功能可以在count操作中添加日志或者输出语句进行测试。
3.定时任务报表开发
当前已经实现了在业务功能类中记录访问数据,但是还没有输出监控的信息到控制台。由于监控信息需要每10秒输出1次,因此需要使用定时器功能。可以选取第三方技术Quartz实现,也可以选择Spring内置的task来完成此功能,此处选用Spring的task作为实现方案。
步骤一:开启定时任务功能
定时任务功能开启需要在当前功能的总配置中设置,结合现有业务设定,比较合理的位置是设置在自动配置类上。加载自动配置类即启用定时任务功能。
@EnableScheduling
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
步骤二:制作显示统计数据功能
定义显示统计功能的操作print(),并设置定时任务,当前设置每5秒运行一次统计数据。
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
//把map中的数据显示出来,做一点展示的处理
@Scheduled(cron = "0/5 * * * * ?") //设置定时任务,每5秒执行一次
public void print(){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
//对map集合的数据进行循环展示
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
//对数据进行格式展示的效果进行调整
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}
}
其中关于统计报表的显示信息拼接可以使用各种形式进行,此处使用String类中的格式化字符串操作进行,学习者可以根据自己的喜好调整实现方案。
温馨提示
每次运行效果之前先clean然后install,切记切记!!
当前效果
每次调用分页操作后,可以在控制台看到统计数据,到此基础功能已经开发完毕。
4.使用属性配置设置功能参数
由于当前报表显示的信息格式固定,为提高报表信息显示的灵活性,需要通过yml文件设置参数,控制报表的显示格式。
步骤一:定义参数格式
设置3个属性,分别用来控制显示周期(cycle),阶段数据是否清空(cycleReset),数据显示格式(model)
tools:
ip:
cycle: 10
cycleReset: false
model: "detail"
步骤二:定义封装参数的属性类,读取配置参数
为防止项目组定义的参数种类过多,产生冲突,通常设置属性前缀会至少使用两级属性作为前缀进行区分。
日志输出模式是在若干个类别选项中选择某一项,对于此种分类性数据建议制作枚举定义分类数据,当然使用字符串也可以。
@ConfigurationProperties(prefix = "tools.ip") //从配置文件去获取值
//注意这个注解配置的类要求该类是spring管理的bean,所以你要么就是使用@Component把这个类声明为bean,
// 要么就是在使用这个类的地方使用@EnableConfigurationProperties(IpProperties.class)
public class IpProperties {
/**
* 日志显示周期
*/
private Long cycle = 5L;
/**
* 是否周期内重置数据
*/
private Boolean cycleReset = false;
/**
* 日志输出模式 detail:详细模式 simple:极简模式
当你需要的值需要 在某些选择中进行,那么使用枚举是更加专业的
*/
private String model = LogModel.DETAIL.value;
public enum LogModel{
DETAIL("detail"),
SIMPLE("simple");
private String value;
LogModel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
步骤三:加载属性类
@EnableScheduling
@EnableConfigurationProperties(IpProperties.class) //把配置文件加载为容器中的bean
public class IpAutoConfiguration {
@Bean
public IpCountService ipCountService(){
return new IpCountService();
}
}
步骤四:应用配置属性
在应用配置属性的功能类中,使用自动装配加载对应的配置bean,然后使用配置信息做分支处理。
注意:清除数据的功能一定要在输出后运行,否则每次查阅的数据均为空白数据。
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();
@Autowired
private IpProperties ipProperties;
//把map中的数据显示出来,做一点展示的处理
@Scheduled(cron = "0/5 * * * * ?") //设置定时任务,每5秒执行一次
public void print(){
//进行模式切换
if (ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){
//明细模式
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
//对map集合的数据进行循环展示
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
//对数据进行格式展示的效果进行调整
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}else if (ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){
//极简模式
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+");
//对map集合的数据进行循环展示
for (String key : ipCountMap.keySet()) {
//对数据进行格式展示的效果进行调整
System.out.println(String.format("|%18s |",key));
}
System.out.println("+--------------------+-------+");
}
//周期类重置数据
if (ipProperties.getCycleReset()){
//从配置中获取是否要清除数据 ,注意要先展示数据再清除数据
ipCountMap.clear();
}
}
}
温馨提示
每次运行效果之前先clean然后install,切记切记!!
当前效果
在web程序端可以通过控制yml文件中的配置参数对统计信息进行格式控制。但是数据显示周期还未进行控制。
5.使用属性配置设置定时器参数(掌握里面的思想)
在使用属性配置中的显示周期数据时,遇到了一些问题。
我们先看看能不能在cron表达式上来得到我们想要的结果:
@Scheduled(cron = "0/${tools.ip.cycle} * * * * ?") //如果用户没有帮我们这个配置文件的这个属性设置值,那不就报错了吗?
public void print(){
}
@Scheduled(cron = "0/${tools.ip.cycle:5} * * * * ?")
/**
表示如果没有从配置文件中读取到cycle这个属性,那么就是用默认值5.
但是吧,如果你这样设置的话,那么你在IpProperties类中开始定义的cycle这个属性还有什么用吗?压根没有被读取到,也就是说你的IpProperties这个bean有没有这个cycle属性都不影响这个值得设定,就是说在其他的定时任务也可以随便读tools.ip.cycle,就是说这个tools.ip.cycle属性被暴露在外面,所有的类都可以直接操作它了,这样是非常不合适的!
*/
public void print(){
}
所以我们要使用的方法应该是从具体的bean中去获取tools.ip.cycle这个属性!
由于无法在@Scheduled注解上直接使用配置数据,改用曲线救国的方针,放弃使用@EnableConfigurationProperties注解对应的功能(因为这个注解生成的bean的name是前缀名-全限定名称,我们不好获取到这个beanname),所以这里改成最原始的bean定义格式。
步骤一:@Scheduled注解使用#{}读取bean属性值 这里与${}是读取配置文件中的属性值的!!!注意区分!
此处读取bean名称为ipProperties的bean的cycle属性值
而且要注意另一个坑:
#{abc.cycle} spring 会认为 abc是一个对象,cycle是这个对象的属性
#{全限定名称.属性} 因为全限定的名称中会涉及到很多个包,也就是说spring会把这些包都当做bean,然后发现容器中压根就没有这些bean,所以又会报错!所以是不能直接使用全限定名称来获取属性的!!!
@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?") //设置定时任务,如果用户配置了相关的配置文件属性那就按照用户配置的时间来,如果没有的话那就使用直接默认的配置
public void print(){
//省略代码 和前面的一样
}
步骤二:属性类定义bean并指定bean的访问名称
如果此处不设置bean的访问名称,spring会使用自己的命名生成器生成bean的长名称,无法实现属性的读取
@ConfigurationProperties(prefix = "tools.ip")
//注意这个注解配置的类要求该类是spring管理的bean,所以你要么就是使用@Component把这个类声明为bean,
// 要么就是在使用这个类的地方使用@EnableConfigurationProperties(IpProperties.class)
@Component("ipProperties")
public class IpProperties {
//省略 和前面的一样
}
步骤三:弃用@EnableConfigurationProperties注解对应的功能,改为导入bean的形式加载配置属性类
@Import({IpCountService.class,IpProperties.class})//相当于把这些类加载到bean中,并且扫描这些类中的注解
@EnableScheduling //开启定时任务
//@EnableConfigurationProperties(IpProperties.class) //把配置文件加载为容器中的bean
public class IpAutoConfiguration {
}
运行测试的时候是 通过修改使用这个依赖的程序的配置文件来完成测试的!
# 测试自己定义的stater
tools:
ip:
cycle: 1
cycleReset: true
model: "simple"
温馨提示
每次运行效果之前先clean然后install,切记切记!!
当前效果
在web程序端可以通过控制yml文件中的配置参数对统计信息的显示周期进行控制
6.拦截器开发
知识补充-具有扫描注解功能的注解(重要)
@componentScan 的作用就是根据定义的扫描路径,把符合扫描规则的类装配到spring容器中,就是把声明好的bean,加载到spring容器中(前提是要扫描到这些声明的bean);
@import
用于直接往容器中导入某个组件,与@Bean注册组件类似,默认名称是全限定类名, 还可以把导入的bean(类)加载到容器中(就是往容器中注册这个bean);
@EnableConfigurationProperties:就是把一个使用了ConfigurationProperties 注解的类加载为一个bean,bean的name是属性的前缀名-全限定名称,同时会把这个bean注册到spring容器中;以及各种带@Enable...开头的注解,都是把对应的bean往容器中注册;
注意:如果我们只是在代码中定义了bean,或者说是声明了bean,那这个时候这个bean是还没有往spring容器中注册的,需要相关的扫描组件把声明的bean加载到容器中;扫描到了才能把bean注册到容器中!!!
@Configuration的作用就是说明这是一个bean,可理解为用spring的时候xml里面的< beans>标签,但是如果你想要这个bean加载的话,你还需要使用扫描组件来扫描这个配置类:
用于告诉SpringBoot这是一个配置类(类似于Spring的xml配置文件)
可以在配置类中注册组件,默认是单实例,配置类本身也是一个组件
@ConfigurationProperties(prefix = "tools.ip") //从配置文件去获取值注入到当前bean的属性中
基础功能基本上已经完成了制作,下面进行拦截器的开发。开发时先在web工程中制作,然后将所有功能挪入starter模块中
步骤一:开发拦截器
使用自动装配加载统计功能的业务类,并在拦截器中调用对应功能,因为我们不可能每次都是手动的调用这个计数的方法的,所以这个要使用拦截器!拦截器是要写在客户端的,就是使用我们这个依赖的程序中的,这个拦截类需要客户端自己配置;
public class IpCountInterceptor implements HandlerInterceptor {
@Autowired
private IpCountService ipCountService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
ipCountService.count();
return true;
}
}
步骤二:配置拦截器,要配置后拦截器才能成功
配置mvc拦截器,设置拦截对应的请求路径。此处拦截所有请求,用户可以根据使用需要设置要拦截的请求。甚至可以在此处加载IpCountProperties中的属性,通过配置设置拦截器拦截的请求。
注意:我们这里使用@Configuration的时候只是帮我们把SpringMvcConfig这个类声明为bean,但是这个类上的注解并没有被扫描,所以是没有生效的,所以我们需要在其他地方设置要扫描这个类才能让这个类的注解生效,所以我们要去IpAutoConfiguration类添加一个导入类;
@Import({IpCountService.class,IpProperties.class, SpringMvcConfig.class})//相当于把这些类加载到bean中,并且扫描这些类中的注解
@EnableScheduling //开启定时任务
//@EnableConfigurationProperties(IpProperties.class) //把配置文件加载为容器中的bean
public class IpAutoConfiguration {
}
@Configuration(proxyBeanMethods = true) //把创建的bean声明为单例的,实际默认就是ture,并且bean的name是以方法名定义的
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器和拦截路径
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
@Bean
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}
温馨提示
每次运行效果之前先clean然后install,切记切记!!
当前效果
在web程序端导入对应的starter后功能开启,去掉坐标后功能消失,实现自定义starter的效果。
到此当前案例全部完成,自定义stater的开发其实在第一轮开发中就已经完成了,就是创建独立模块导出独立功能,需要使用的位置导入对应的starter即可。如果是在企业中开发,记得不仅需要将开发完成的starter模块install到自己的本地仓库中,开发完毕后还要deploy到私服上,否则别人就无法使用了。
7.功能性完善——开启yml提示功能
我们在使用springboot的配置属性时,都可以看到提示,尤其是导入了对应的starter后,也会有对应的提示信息出现。但是现在我们的starter没有对应的提示功能,这种设定就非常的不友好,本节解决自定义starter功能如何开启配置提示的问题。
springboot提供有专用的工具实现此功能,仅需要导入下列坐标。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
总结
-
自定义starter其实就是做一个独立的功能模块,核心技术是利用自动配置的效果在加载模块后加载对应的功能
-
通常会为自定义starter的自动配置功能添加足够的条件控制,而不会做成100%加载对功能的效果
-
本例中使用map保存数据,如果换用redis方案,在starter开发模块中就要导入redis对应的starter
-
对于配置属性务必开启提示功能,否则使用者无法感知配置应该如何书写
本案例以及相关的笔记是基于学习 b站黑马李老师的spring Boot2 课程中的原理篇产生的!