使用spring事件驱动机制,实现权限信息的自动收集存库

最近在做权限管理,希望能够在容器初始化后,自动的将所有Controller上的url进行收集,并存放到权限表中,从而代替人工配置的方式。实现思路如下:         

                    spring事件驱动机制+注解来实现。

spring事件驱动机制

spring在容器初始化之后,会触发ContextRefreshedEvent等事件,只要实现了ApplicationListener就可以捕获这个事件,这个时候,我们就可以做很多事情了,比如权限信息的提取,加载缓存等等,所以,我们的思路也是基于此的。

1、自定义注解

  1. import java.lang.annotation.ElementType;  
  2. import java.lang.annotation.Retention;  
  3. import java.lang.annotation.RetentionPolicy;  
  4. import java.lang.annotation.Target;  
  5.   
  6. /** 
  7.  * 权限注解,后台会在容器中所有的类都加载完毕之后,通过扫描该注解,将系统中所有url对应的权限信息存到数据库中, 
  8.  * 减少人为配置 
  9.  * 注意:之所以加该注解而不直接用RequestMapping原因如下: 
  10.  * 1、经测试,classpath下其他的框架中也可能存在使用RequestMapping的情况,这样就会出错,例如org.springframework.boot.autoconfigure.web.BasicErrorController类 
  11.  * 2、RequestMapping没有其他的需要的信息,用起来也不灵活 
  12.  * @author chhliu 
  13.  * 
  14.  */  
  15. @Retention(RetentionPolicy.RUNTIME)  
  16. @Target({ElementType.METHOD, ElementType.TYPE})  
  17. public @interface PermissionPath {  
  18.     // 权限路径,需要和RequestMapping里面的路径一致   
  19.     public String path() default "";  
  20.     // 当前Controller对应的模块名称   
  21.     public String moduleName() default "";  
  22.     // 当前Url对应的操作   
  23.     public Operation operation();  
  24. }  
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 权限注解,后台会在容器中所有的类都加载完毕之后,通过扫描该注解,将系统中所有url对应的权限信息存到数据库中,
 * 减少人为配置
 * 注意:之所以加该注解而不直接用RequestMapping原因如下:
 * 1、经测试,classpath下其他的框架中也可能存在使用RequestMapping的情况,这样就会出错,例如org.springframework.boot.autoconfigure.web.BasicErrorController类
 * 2、RequestMapping没有其他的需要的信息,用起来也不灵活
 * @author chhliu
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface PermissionPath {
	// 权限路径,需要和RequestMapping里面的路径一致
	public String path() default "";
	// 当前Controller对应的模块名称
	public String moduleName() default "";
	// 当前Url对应的操作
	public Operation operation();
}

  1. /** 
  2.  * controller中url对应的操作权限,目前限定的操作为4种 
  3.  * @author chhliu 
  4.  */  
  5. public enum Operation {  
  6.     VIEW, UPDATE, ADD, DELETE  
  7. }  
/**
 * controller中url对应的操作权限,目前限定的操作为4种
 * @author chhliu
 */
public enum Operation {
	VIEW, UPDATE, ADD, DELETE
}

2、实现ApplicationListener

  1. import java.lang.reflect.Method;  
  2. import java.util.Map;  
  3. import java.util.Map.Entry;  
  4.   
  5. import org.springframework.context.ApplicationContext;  
  6. import org.springframework.context.ApplicationListener;  
  7. import org.springframework.context.event.ContextRefreshedEvent;  
  8. import org.springframework.core.annotation.AnnotationUtils;  
  9. import org.springframework.stereotype.Component;  
  10.   
  11. import com.chhliu.srd.rdcloud.annotation.PermissionPath;  
  12.   
  13. /** 
  14.  * spring的事件驱动机制 
  15.  * 实现ApplicationListener后,容器会在初始化所有的bean之后,触发refresh事件 
  16.  * 所以在该方法中,可以实现对系统中所有Controller中url的提取 
  17.  * @author chhliu 
  18.  * 
  19.  */  
  20. @Component  
  21. public class ExtractPermissionInformationContext implements ApplicationListener<ContextRefreshedEvent> {  
  22.   
  23.     /** 
  24.      * refresh事件 
  25.      */  
  26.     @Override  
  27.     public void onApplicationEvent(ContextRefreshedEvent event) {  
  28.           
  29.         System.out.println("===============开始提取权限信息================");  
  30.         System.out.println(event.getTimestamp());  
  31.         // 通过event获取到spring 的ApplicationContext上下文   
  32.         ApplicationContext applicationContext = event.getApplicationContext();  
  33.           
  34.         // 获取到所有Bean上标记有PermissionPath注解的Bean   
  35.         Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PermissionPath.class);  
  36.         // 判空   
  37.         if(null != beansWithAnnotation && !beansWithAnnotation.isEmpty()){  
  38.             for(Entry<String, Object> entry:beansWithAnnotation.entrySet()){  
  39.                 // 获取带指定注解的类   
  40.                 Class<? extends Object> clz = entry.getValue().getClass();  
  41.                 System.out.println("className:"+clz.getName());  
  42.                 // 通过类获取到类上注解的信息   
  43.                 PermissionPath rm = AnnotationUtils.findAnnotation(clz, PermissionPath.class);  
  44.                 System.out.println("class requestMapping:"+rm.path());  
  45.                   
  46.                 // 获取该类下的所有方法   
  47.                 Method[] methods = clz.getMethods();  
  48.                 for(Method m:methods){  
  49.                     // 如果该方法上标记了对应的注解,则是我们需要提取权限信息的方法   
  50.                     if(m.isAnnotationPresent(PermissionPath.class)){  
  51.                         // 获取方法上注解的信息   
  52.                         PermissionPath ma = m.getAnnotation(PermissionPath.class);  
  53.                         System.out.println("method requestMapping:"+ma.path());  
  54.                     }  
  55.                 }  
  56.             }  
  57.         }  
  58.         System.out.println("===============开始提取权限信息================");  
  59.         System.out.println(event.getTimestamp());  
  60.     }  
  61. }  
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import com.chhliu.srd.rdcloud.annotation.PermissionPath;

/**
 * spring的事件驱动机制
 * 实现ApplicationListener后,容器会在初始化所有的bean之后,触发refresh事件
 * 所以在该方法中,可以实现对系统中所有Controller中url的提取
 * @author chhliu
 *
 */
@Component
public class ExtractPermissionInformationContext implements ApplicationListener<ContextRefreshedEvent> {

	/**
	 * refresh事件
	 */
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		
		System.out.println("===============开始提取权限信息================");
		System.out.println(event.getTimestamp());
		// 通过event获取到spring 的ApplicationContext上下文
		ApplicationContext applicationContext = event.getApplicationContext();
		
		// 获取到所有Bean上标记有PermissionPath注解的Bean
		Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PermissionPath.class);
		// 判空
		if(null != beansWithAnnotation && !beansWithAnnotation.isEmpty()){
			for(Entry<String, Object> entry:beansWithAnnotation.entrySet()){
				// 获取带指定注解的类
				Class<? extends Object> clz = entry.getValue().getClass();
				System.out.println("className:"+clz.getName());
				// 通过类获取到类上注解的信息
				PermissionPath rm = AnnotationUtils.findAnnotation(clz, PermissionPath.class);
				System.out.println("class requestMapping:"+rm.path());
				
				// 获取该类下的所有方法
				Method[] methods = clz.getMethods();
				for(Method m:methods){
					// 如果该方法上标记了对应的注解,则是我们需要提取权限信息的方法
					if(m.isAnnotationPresent(PermissionPath.class)){
						// 获取方法上注解的信息
						PermissionPath ma = m.getAnnotation(PermissionPath.class);
						System.out.println("method requestMapping:"+ma.path());
					}
				}
			}
		}
		System.out.println("===============开始提取权限信息================");
		System.out.println(event.getTimestamp());
	}
}

测试结果如下:

  1. ===============开始提取权限信息================  
  2. 1503631242290  
  3. className:com.chhliu.srd.rdcloud.contractmanager.contractimport.controller.ContractImportController  
  4. class requestMapping:/api/contractimport  
  5. method requestMapping:updateMilston  
  6. method requestMapping:queryDepartMent  
  7. method requestMapping:queryByCondition  
  8. method requestMapping:/fileUpload  
===============开始提取权限信息================
1503631242290
className:com.chhliu.srd.rdcloud.contractmanager.contractimport.controller.ContractImportController
class requestMapping:/api/contractimport
method requestMapping:updateMilston
method requestMapping:queryDepartMent
method requestMapping:queryByCondition
method requestMapping:/fileUpload

通过上面的几个步骤,就基本实现了对权限信息的自动提取工作。那我们更近一步,spring的事件驱动机制是怎么工作的了,下面我们来看一个简单的示例。

spring事件驱动流程:spring的事件传播机制 是基于观察者模式(Observer)实现的,它可以将 Spring Bean 的改变定义为事件 ApplicationEvent,通过 ApplicationListener 监听 ApplicationEvent 事件,一旦Spring Bean 使用 ApplicationContext.publishEvent( ApplicationEvent event )发布事件后,Spring 容器会通知注册在容器中的所有 ApplicationListener 接口的实现类,最后 ApplicationListener 接口实现类判断是否响应刚发布出来的 ApplicationEvent 事件。

从上面的流程原理这段描述中,我们可以发现几个关键类:1、ApplicationEvent,2、ApplicationListener

所以,在使用中,会有如下几个步骤:

1、建立事件类,即我们对外要发布的事件

2、建立监听类,监听第一步中发布的事件

3、发布事件

下面我们就通过一个简单的示例来逐步实现,示例需求:用户在注册成功之后,发布通知事件,通知采取短信和邮件两种通知方式,当事件监听器监听到发布的事件之后,通知用户注册成功。

建立事件类:通过继承ApplicationEvent类来实现,示例代码如下:

  1. package com.chhliu.application;  
  2.   
  3. import org.springframework.context.ApplicationEvent;  
  4.   
  5. public class SendEvent extends ApplicationEvent {  
  6.       
  7.     // 邮件或者是短信主题   
  8.     private String title;  
  9.       
  10.     // 邮件或者是短信发送人   
  11.     private String sender;  
  12.       
  13.     // 邮件或短信接收人   
  14.     private String receiver;  
  15.       
  16.     // 邮件或短信内容   
  17.     private String message;  
  18.     /** 
  19.      *  
  20.      */  
  21.     private static final long serialVersionUID = 1L;  
  22.   
  23.     public SendEvent(String source) {  
  24.         super(source);  
  25.     }  
  26.       
  27.     public SendEvent(String source, String title, String sender, String receiver, String message){  
  28.         super(source);  
  29.         this.title = title;  
  30.         this.sender = sender;  
  31.         this.receiver = receiver;  
  32.         this.message = message;  
  33.     }  
  34. }  
package com.chhliu.application;

import org.springframework.context.ApplicationEvent;

public class SendEvent extends ApplicationEvent {
	
	// 邮件或者是短信主题
	private String title;
	
	// 邮件或者是短信发送人
	private String sender;
	
	// 邮件或短信接收人
	private String receiver;
	
	// 邮件或短信内容
	private String message;
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public SendEvent(String source) {
		super(source);
	}
	
	public SendEvent(String source, String title, String sender, String receiver, String message){
		super(source);
		this.title = title;
		this.sender = sender;
		this.receiver = receiver;
		this.message = message;
	}
}

建立监听类,并监听事件,这里我们创建两个监听类,分别是短信通知监听和邮件通知监听。

短信通知监听示例代码如下:

  1. package com.chhliu.application;  
  2.   
  3. import org.springframework.context.ApplicationListener;  
  4. import org.springframework.stereotype.Component;  
  5.   
  6. @Component  
  7. public class MessageSendListener implements ApplicationListener<SendEvent> {  
  8.   
  9.     @Override  
  10.     public void onApplicationEvent(SendEvent event) {  
  11.         System.out.println(event.getSource()+":注册成功,发送短信通知!");  
  12.           
  13.         System.out.println("发送短信到:"+event.getReceiver()+" 发送人:"+event.getSender()+" 短信内容:"+event.getMessage());  
  14.     }  
  15.   
  16. }  
package com.chhliu.application;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MessageSendListener implements ApplicationListener<SendEvent> {

	@Override
	public void onApplicationEvent(SendEvent event) {
		System.out.println(event.getSource()+":注册成功,发送短信通知!");
		
		System.out.println("发送短信到:"+event.getReceiver()+" 发送人:"+event.getSender()+" 短信内容:"+event.getMessage());
	}

}

邮件通知监听示例代码如下:

  1. package com.chhliu.application;  
  2.   
  3. import org.springframework.context.ApplicationListener;  
  4. import org.springframework.stereotype.Component;  
  5.   
  6. @Component  
  7. public class MailSendListener implements ApplicationListener<SendEvent> {  
  8.   
  9.     @Override  
  10.     public void onApplicationEvent(SendEvent event) {  
  11.         System.out.println(event.getSource()+":注册成功,发送邮件通知!");  
  12.           
  13.         System.out.println("发送邮件到:"+event.getReceiver()+" 发送人:"+event.getSender()+" 邮件内容:"+event.getMessage());  
  14.     }  
  15.   
  16. }  
package com.chhliu.application;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MailSendListener implements ApplicationListener<SendEvent> {

	@Override
	public void onApplicationEvent(SendEvent event) {
		System.out.println(event.getSource()+":注册成功,发送邮件通知!");
		
		System.out.println("发送邮件到:"+event.getReceiver()+" 发送人:"+event.getSender()+" 邮件内容:"+event.getMessage());
	}

}

发布事件:经过上面的几个步骤,事件源和事件监听就做好了,剩下的就是发布事件了,spring中通过实现ApplicationContextAware类来发布事件,示例代码如下:

  1. package com.chhliu.application;  
  2.   
  3. import org.springframework.beans.BeansException;  
  4. import org.springframework.context.ApplicationContext;  
  5. import org.springframework.context.ApplicationContextAware;  
  6. import org.springframework.stereotype.Service;  
  7.   
  8. @Service  
  9. public class Registration implements ApplicationContextAware {  
  10.   
  11.     private ApplicationContext applicationContext;  
  12.       
  13.       
  14.     @Override  
  15.     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
  16.         this.applicationContext = applicationContext;  
  17.     }  
  18.       
  19.     public void register(final String username, final String password){  
  20.         System.out.println("调用后台注册服务!");  
  21.         System.out.println("注册成功,发送邮件和短信通知!");  
  22.           
  23.         // 创建待发布的事件   
  24.         SendEvent se = new SendEvent(username, "注册会员成功通知""chhliu", username, "恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!");  
  25.         // 发布事件   
  26.         this.applicationContext.publishEvent(se);  
  27.     }  
  28. }  
package com.chhliu.application;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

@Service
public class Registration implements ApplicationContextAware {

	private ApplicationContext applicationContext;
	
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
	
	public void register(final String username, final String password){
		System.out.println("调用后台注册服务!");
		System.out.println("注册成功,发送邮件和短信通知!");
		
		// 创建待发布的事件
		SendEvent se = new SendEvent(username, "注册会员成功通知", "chhliu", username, "恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!");
		// 发布事件
		this.applicationContext.publishEvent(se);
	}
}

测试代码如下:

  1. package com.chhliu;  
  2.   
  3. import org.junit.Test;  
  4. import org.junit.runner.RunWith;  
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6. import org.springframework.boot.test.context.SpringBootTest;  
  7. import org.springframework.test.context.junit4.SpringRunner;  
  8.   
  9. import com.chhliu.application.Registration;  
  10.   
  11. @RunWith(SpringRunner.class)  
  12. @SpringBootTest  
  13. public class SpringStudyApplicationTests {  
  14.       
  15.     @Autowired  
  16.     private Registration regis;  
  17.       
  18.     @Test  
  19.     public void contextLoads() {  
  20.         regis.register("xyh""123456");  
  21.           
  22.     }  
  23.   
  24. }  
package com.chhliu;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.chhliu.application.Registration;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringStudyApplicationTests {
	
	@Autowired
	private Registration regis;
	
	@Test
	public void contextLoads() {
		regis.register("xyh", "123456");
		
	}

}

测试结果如下:

  1. 调用后台注册服务!  
  2. 注册成功,发送邮件和短信通知!  
  3. xyh:注册成功,发送邮件通知!  
  4. 发送邮件到:xyh 发送人:chhliu 邮件内容:恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!  
  5. xyh:注册成功,发送短信通知!  
  6. 发送短信到:xyh 发送人:chhliu 短信内容:恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!  
调用后台注册服务!
注册成功,发送邮件和短信通知!
xyh:注册成功,发送邮件通知!
发送邮件到:xyh 发送人:chhliu 邮件内容:恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!
xyh:注册成功,发送短信通知!
发送短信到:xyh 发送人:chhliu 短信内容:恭喜您,注册我司会员成功,从今天开始,您将享受我司5星级服务!

从测试结果来看,需求基本上实现了。


扩展:在spring中,也可以通过实现InitializingBean来实现类似的功能,InitializingBean也是在容器初始化之后才会启动,但他们仍然有区别,InitializingBean的方式要先于spring的事件机制,如果我们的需求是要在所有的类都初始化之后,再做一些事情的话,就不能用InitializingBean了。

在平时的框架中,用到了大量的事件驱动机制,比如springmvc中加载所有的Controller,当有请求的时候,对Controller进行拦截,然后进行Controller的匹配以及分发等,spring cloud中的config热部署配置文件等


最后,我们来稍微解析下源码。

spring事件驱动相关的包都在spring-context的org.springframework.context.event包以及org.springframework.context(接口的相关定义)下,截图如下:



这里重点说下ApplicationContextAware这个接口:通过该接口,可以很方便的获得spring的ApplicationContext上下文,并通过ApplicationContext获取到spring容器中定义的Bean,该接口只定义了一个方法,如下:

  1. void setApplicationContext(ApplicationContext applicationContext) throws BeansException;  
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

spring会自动的将ApplicationContext注入到ApplicationContextAware接口的实现类中。  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值