关于手写实现Spring注解实现自定义配置功能

Posted 前进道路上的程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于手写实现Spring注解实现自定义配置功能相关的知识,希望对你有一定的参考价值。

文章目录

前言

我们在使用Spring中,常常使用注解来进行配置,例如用@Controller来注解这个类为控制器类,用@Service来注解这个是Service层类,@Autowired来表示对象注入,@RequestParam来表示对象为一个参数等等,那这些功能在Spring中是怎么实现的呢,我们就手动来实现一下

配置准备

配置application.properties

我们需要在src目录下准备一个application.properties文件,并且里面写上scanPackage=com.mvcframework

配置web.xml

我们在web.xml中配置相应的servlet,并且将application.properties作为参数加入到到这servlet里

<servlet>
     <servlet-name>gpmvc</servlet-name>
     <servlet-class>com.mvcframework.v1.servlet.GPDispatchServlet</servlet-class>
     <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>application.properties</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
     <servlet-name>gpmvc</servlet-name>
     <url-pattern>/*</url-pattern>
 </servlet-mapping>

自定义注解

@GPController

GPController :

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPController 
    String value() default "";

@GPService

GPService:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPService 
    String value() default "";

@GPAutowired

GPAutowired:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPAutowired 
    String value() default "";

@GPRequestMapping

GPRequestMapping :

@Target(ElementType.TYPE,ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestMapping 
    String value() default "";

@GPRequestParam

GPRequestParam:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestParam 
    String value() default "";

应用代码

应用的代码如下

DemoAction :

@GPController
@GPRequestMapping("/demo")
public class DemoAction 
    @GPAutowired
    private IDemoService demoService;
    @GPRequestMapping("/query")
    public void query(HttpServletRequest req, HttpServletResponse resp, @GPRequestParam("name") String name) 
        String result = demoService.get(name);
        try
            resp.getWriter().write(result);
         catch (IOException e) 
            e.printStackTrace();
        
    

@GPService
public class DemoSerevice implements IDemoService
    public String get(String name) 
        return "My name is "+name;
    

public interface IDemoService 
    public String get(String name);

这些代码中DemoAction 使用@GPController定义其为GPController,然后注解@GPRequestMapping("/demo")定义其类路径,里面有一个IDemoService 属性使用@GPAutowired进行注解,一个方法query使用@GPRequestMapping("/query")定义其方法路径,方法中参数name使用@GPRequestParam(“name”)定义其参数
DemoSerevice 类使用@GPService定义其为GPService,同时实现IDemoService接口

容器初始化

接下来我们就要设计实现Spring注解访问资源的核心功能,这里我们就需要用到容器
在最早没有Spring的时候,我们实现MVC的时候,一般是通过从web.xml配置中,进入到相应的servlet,这里实现的功能核心就是在servlet的init方法里实现的
主要步骤包括:
1、加载配置文件
2、扫描相关的类
3、初始化扫描到的类,并且将它们放入容器中
4、完成依赖注入
5、初始化HandlerMapping,实现url与访问方法的匹配
然后就是在访问的时候根据url从HandlerMapping中获取相应的方法并执行
话不多说,我们用代码来看看

声明全局变量

首先我们在Servlet开头定义各种变量

//保存application.properties配置文件的内容
	private Properties contextConfig= new Properties();
//保存扫描的所有类名
    private List<String> classNames = new ArrayList<>();
//ioc容器,用于保存扫描到的类
    private Map<String,Object> ioc = new HashMap<String,Object>();
//保存url和Method的对应关系
    private Map<String, Method> handlerMapping = new HashMap<String,Method>();

init中定义调用方法的步骤

这里使用到了模板方法模式,定义了初始化时各种方法的使用步骤

 @Override
    public void init(ServletConfig config) throws ServletException 
//1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2、扫描相关的类
        doScanner(contextConfig.getProperty("scanPackage"));
//初始化扫描到的类,并存入Ioc容器
        doInstance();
//依赖注入
        doAutowired();
//初始化HandlerMapping,实现url与访问方法的匹配
        initHandlerMapping();
    

下面我们一步一步完成每个相关方法

加载配置文件

private void doLoadConfig(String contextConfigLocation) 
        InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try
            contextConfig.load(fis);
         catch (IOException e) 
            e.printStackTrace();
         finally 
            if(null != fis) 
                try
                    fis.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
    

这个方法中,我们我们将配置的application.properties内容加载进contextConfig变量

扫描相关的类

private void doScanner(String scanPackage) 
        URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\\\.","/"));
        File classPath = new File(url.getFile());
        for(File file:classPath.listFiles()) 
            if(file.isDirectory()) 
                doScanner(scanPackage+"."+file.getName());
             else 
                if(!file.getName().endsWith(".class")) continue;
                String className = (scanPackage+"."+file.getName().replace(".class",""));
                classNames.add(className);
            
        
    

这个类中,通过递归不断的将application.properties中配置的路径下的类路径扫描入classNames中

初始化扫描到的类,并存入Ioc容器

private void doInstance()
        if(classNames.isEmpty()) return;
        try
            for(String className:classNames) 
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(GPController.class)) 
                    Object instance = clazz.newInstance();
                    String beanName = toLowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName,instance);
                 else if(clazz.isAnnotationPresent(GPService.class)) 
                    GPService service = clazz.getAnnotation(GPService.class);
                    String beanName = service.value();
                    if("".equals(beanName.trim())) 
                        beanName = toLowerFirstCase(clazz.getSimpleName());
                    
                    Object instance = clazz.newInstance();
                    ioc.put(beanName,instance);
                    for(Class<?> i:clazz.getInterfaces()) 
                        if(ioc.containsKey(i.getName())) 
                            throw new Exception("The “"+ i.getName() +"” is exists!! ");
                        
                        ioc.put(i.getName(),instance);
                    
                 else 
                    continue;
                
            
        catch (Exception e) 
            e.printStackTrace();
        
    

在这里面判断注释类型使用了isAnnotationPresent,这是一个很重要的方法
这个方法中,通过不断循环classNames中的类名,然后根据其类的相应注释来进行判断,从而使用反射来生成相应的对象,并且里面判断其注释类型,如果为GPController则存放到Ioc里的key使用类名首字母小写;如果为GPService则判断有没有写value,没有则使用类名首字母小写作为key,并且,Service的接口类也要存放,其key为接口类路径
执行这个方法后,ioc容器内情况如下:

类名首字母小写方法:

private String toLowerFirstCase(String simpleName) 
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    

依赖注入

private void doAutowired() 
        if(ioc.isEmpty()) return;
        for(Map.Entry<String,Object> entry:ioc.entrySet()) 
            Field[] fields = entry.getValue().getClass().getDeclaredFields();
            for (Field field:fields) 
                if(!field.isAnnotationPresent(GPAutowired.class))continue;
                GPAutowired autowired = field.getAnnotation(GPAutowired.class);
                String beanName = autowired.value().trim();
                if("".equals(beanName)) 
                    beanName = field.getType().getName();
                

                field.setAccessible(true);
                try
                    field.set(entry.getValue(),ioc.get(beanName));//TODO
                 catch (IllegalAccessException e) 
                    e.printStackTrace();
                
            
        
    

在这个方法中,我们会轮行取出ioc容器中的对象,然后轮训对象中的属性,如果属性以GPAutowired注释,则利用其备注的Value或者类型名称去ioc容器里查找对应的对象注入该属性中
执行完这个方法后,ioc容器状态如下

初始化HandlerMapping

private void initHandlerMapping() 
        if(ioc.isEmpty()) 
            return;
        
        for(Map.Entry<String,Object> entry:ioc.entrySet()) 
            Class<?> clazz = entry.getValue().getClass();
            if(!clazz.isAnnotationPresent(GPController.class)) 
                continue;
            
            String baseUrl = "";
            if(clazz.isAnnotationPresent(GPRequestMapping.class)) 
                GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
                baseUrl = requestMapping.value();
            

            for (Method method:clazz.getMethods()) 
                if(!method.isAnnotationPresent(GPRequestMapping.class))continue;
                GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
                String url = ("/"+baseUrl+"/"+requestMapping.value()).replaceAll("/+","/");
                handlerMapping.put(url,method);
                System.out.println("Mapped :"+url+","+method);
            
        
    

在这个方法中,我们从ioc容器中拿出GPController修饰的对象,然后其类上注释的GPRequestMapping路径叠加方法上叠加的GPRequestMapping路径,然后生成url作为key,method作为value存放到handlerMapping中
执行完这个方法handlerMapping状态如下:

访问时调用

@Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
        this.doPost(request,response);
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
        try 
            doDispatch(request,response);
         catch (Exception e)
            response.getWriter().write("500 Exception "+ Arrays.toString(e.getStackTrace()));
        
    

我们在servlet中定义doGet和doPost如上面,然后doPost里调用doDispatch,

doDispatch方法如下

    private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception 
        String url = request.getRequestURI();
        String contextPath = request.getContextPath关于手写实现Spring注解实现自定义配置功能

手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能

Spring CloudSpring Cloud之自定义@SpringCloudProfile注解实现@Profile注解的功能

Spring系列之手写一个SpringMVC

手写一个简易的IOC

spring AOP自定义注解方式实现日志管理