Saturday, May 21, 2011

Dissension of Dependency injection

Dependency injection [Part 3]

Dependency Injection (DI)


Part 1 describes the basic concept of Dependency Injection and quick overview of spring framework.
Annotation and its strength described on blog Annotation, A Journey Novice to Ninja.
Part 2 describes the annotation based dependency injection.
First we focus on basic concept to implements annotation based dependency injection.
What is package?
A Java package is a mechanism for organizing group of related Java classes to single namespace.
1)      Package defines a name space in which classes are stored.
2)      By default all classes belongs to default package.
3)      Package declaration needs to be first statement on source file else compiler generates the compile time error.
4)      Example :Package  java.io contains input-output related classes and directory structure for java.io is /java/io
5

Let’s we write a function which finds number of files in a specific folder including its sub-folders.

Directory structures are as follow for the code snippet 1

Code snippet 1


This code recursively iterate the specified directory to scan all the files.
package com.ashish.test;

import java.io.File;

/**
 * @author Ashish.Chudasama
 */
public class DirectoryExplorer
{

    public static void main(String[] args)
    {
scanDirectory(new File("E:\\workspacei18n\\DISpringAnnotation\\bin\\com"));
    }

    /**
     * Recursively iterate the directory.
     */
    static void scanDirectory(File dir)
    {
        if (dir != null)
        {
            if (!dir.exists()) {
                return ;
            }
            //System.out.println(dir.getPath());
            File[] listFiles = dir.listFiles();
            for (File dirName : listFiles)
            {
                if (dirName.isDirectory())
                {
                    scanDirectory(dirName);
                }
                else
                {
                    if (dirName.isFile())
                    {
                        System.out.println(" " + dirName.getName());
                    }
                }
            }
        }
    }
}

Output
DirectoryExplorer.class
 Main.class
 ScanPackageClasses.class
 Test$InnerTest.class
 Test.class
 Bus.class
 Car.class
 TestSpringDI.class
 Traveler.class
 Vehicle.class
 Traveler.class
 TravelerImprove.class
 TravelerImproveOne.class
 Bus.class
 Car.class
 JKTire.class
 MRFTire.class
 Tire.class
 Wheel.class


Key points:
1)      During source code development package represent as directory structure and can be iterate using file objects.
2)      The code snippet will not work for the jar file. We have to write code that iterates the zip files.
3)      Code snippet 1 accepts the absolute path to iterate the directory structure.

Code snippet 2



Code snippet 1 scan file based on absolute path whereas code snippet 2 scan file based on package name.

package org.ashish.di;

import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
/**
 * @author Ashish.Chudasama
 */
public class ScanPackageClasses
{

    public static void main(String[] args) throws Exception
    {
        List<Class< Object >> scanPackage = scanPackage("org.dom4j.bean");
    for (Class<?> clz : scanPackage)
    {
        System.out.println(clz.getName());
    }
}

private static List<Class< Object >> scanPackage(String packageName) throws Exception
{
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    assert classLoader != null;
    String path = packageName.replace('.', '/');
    Enumeration<URL> resources = classLoader.getResources(path);
    List<File> dirs = new ArrayList<File>();
    List<File> jarDirs = new ArrayList<File>();
    while (resources.hasMoreElements())
    {
        URL resource = resources.nextElement();

        String fileName = resource.getFile();
        String fileNameDecoded = URLDecoder.decode(fileName, "UTF-8");

        if (resource.getProtocol().equalsIgnoreCase("jar"))
        {
            // jar resources
            jarDirs.add(new File(fileNameDecoded));
        }
        else
        {
            // directory resources
            dirs.add(new File(fileNameDecoded));
        }
    }

    ArrayList<Class< Object >> classes = new ArrayList<Class< Object >>();
    for (File directory : dirs)
    {
        classes.addAll(scanDirClasses(directory, packageName));
    }
    for (File directory : jarDirs)
    {
        classes.addAll(scanJarClasses(directory, path));
    }

    return classes;
}

 /**
   * Recursively iterates the directory.
  */
private static List<Class< Object >> scanDirClasses(File dir, String packageName) throws Exception
    {
        List<Class< Object >> classes = new ArrayList<Class<?>>();
        if (dir != null)
        {
            if (!dir.exists())
            {
                return classes;
            }
            File[] listFiles = dir.listFiles();
            for (File dirName : listFiles)
            {
                String fileName = dirName.getName();
                if (dirName.isDirectory())
                {
                    // Recursively scan all classes.
                    classes.addAll(scanDirClasses(dirName, packageName + "." + fileName));
                }
                else
                {
                    if (dirName.isFile())
                    {
                        Class< Object > scanClass = null;
                        // filter all files
                        // Compiler creates FileName$InnerClassName for the                           //inner classes.
// refer only those classes which are not inner //classes.
if (
fileName.endsWith(".class") && !fileName.contains("$"))
                        {
 // scan classes but does not initialized class //until its required.
                        // Here overloaded Class.forName is used which
//Load the classes but does not instantiate the
                            // class objects.
scanClass =(Class<Object>)
Class.forName(packageName + '.'
 + fileName.substring(0, fileName.length() - 6),
false, Thread
                                   .currentThread().getContextClassLoader());
                        
classes.add(scanClass);
                        }
                    }
                }
            }
        }
        return classes;
    }

    /**
     * Iterate all package within Jar.
     */
private static List<Class< Object>> scanJarClasses(File directory, String packageName) throws Exception
    {

        List<Class< Object >> classes = new ArrayList<Class< Object >>();
        JarFile jf = null;

jf = new JarFile(new File(directory.getPath().substring(directory.getPath().indexOf(System.getProperty("file.separator")) + 1,
directory.getPath().indexOf(".jar!" + System.getProperty("file.separator")) + 4)));

        if (jf != null)
        {
            Enumeration<JarEntry> en = jf.entries();
            while (en.hasMoreElements())
            {
                ZipEntry ze = (ZipEntry) en.nextElement();
if (ze.getName().contains(packageName)
&& ze.getName().endsWith(".class")
&& !ze.getName().contains("$"))
                {
                    ClassLoader contextClassLoader =
                    Thread.currentThread().getContextClassLoader();

String substring = ze.getName().replace("/", ".").substring(0, ze.getName().indexOf(".class"));

classes.add((Class<Object>) Class.forName(substring,
 false,
 contextClassLoader));
                }
            }
        }

        return classes;
    }

}


Key points:
1)      ScanPackageClasses class will scan classes from directory / jar files.
2)      Class.forName used to load the class files from specify package.

Now creates a custom Dependency Injection framework.
Annotations supported by Custom Dependency Injection framework are as follow
@Autowired
Resolve dependency automatically while creating object.
@ Qualifier
Used to specify qualify name which helps DI to resolve dependency
@ Service
Represent the service

Let's create the Custom annotation for DI framework

Code snippet 3 

The code snippet 3 shows the @Autowired annotation which is a part of custom DI framework.

package com.ninja.annoation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Ashish.Chudasama
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.CONSTRUCTOR,
    java.lang.annotation.ElementType.FIELD,
    java.lang.annotation.ElementType.METHOD})
public @interface Autowired
{
}

Code snippet 4 

The code snippet 4 shows the @Qualifier annotation which is a part of custom DI framework.

package com.ninja.annoation;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;  
/**
 * @author Ashish.Chudasama
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.FIELD,
java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE })
@Inherited
public @interface Qualifier
{
    public String value( ) default "";
}

Code snippet 5 

The code snippet 5 shows the @Service annotation which is a part of custom DI framework.

package com.ninja.annoation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Ashish.Chudasama
 */
@Target({java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Service
{
    public String value() default "";
}

Code snippet 6 

The DIAwareBeanFactory factory class is the heart of the DI framework which is responsible for creating and resolving any dependency associated with requested bean.

package org.ashish.di;
                                                  
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.ninja.annoation.Autowired;
import com.ninja.annoation.Qualifier;
import com.ninja.annoation.Service;

/**
 * @author Ashish.Chudasama
 * @version $Revision$ Last changed by $Author$ on $Date$ as $Revision$
 */
public class DIAwareBeanFactory
{
    Map<String, Class<Object>> beanMap = new HashMap<String, Class<Object>>();
    List<Class<Object>> allclasses = new ArrayList<Class<Object>>();

    public DIAwareBeanFactory(String[] scanpackage)
    {
        // initializes all base package
        for (String packagename : scanpackage)
        {
            try
            {
                allclasses.addAll(ScanPackageClasses.scanPackage(packagename));
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }

        initializeBeans();
    }

    /**
     * initialize service beans.
     */
    private void initializeBeans()
    {
        for (Class<Object> clz : allclasses)
        {
            String beanName = getBeanName(clz);
            if (beanName != null && !beanName.equals(""))
            {
                System.out.println(beanName);
                beanMap.put(beanName, clz);
            }
        }
    }
  /**
     * This method returns bean name.
     * @param clz class
     * @return beanname
     */
    private String getBeanName(Class<Object> clz)
    {
        String beanname = null;
        Annotation[] annotations = clz.getAnnotations();

        for (Annotation annotation : annotations)
        {
            if (annotation != null && annotation instanceof Service)
            {
                beanname = ((Service) annotation).value();
                if (beanname == null || beanname.trim().equals(""))
                {
                    Qualifier qualifier = clz.getAnnotation(Qualifier.class);
                    if (qualifier != null)
                    {
                        beanname = qualifier.value();
                    }
                    if (beanname == null || beanname.trim().equals(""))
                    {
                        beanname = Character.toLowerCase(clz.getSimpleName().charAt(0)) + clz.getSimpleName().substring(1);
                    }
                }
            }
        }
        return beanname;
    }
  /**
     * This method resolve any bean dependency and returns the bean.
     * This method only works if all class provides default constructor.
     * @param beanName
     * @return
     * @throws Exception
     */
    public Object getBean(String beanName) throws Exception
    {
        Class clz = beanMap.get(beanName);
        Object returnBean = clz.newInstance();
        Method[] declaredMethods = clz.getDeclaredMethods();
        for (Method method : declaredMethods)
        {
            Autowired annotation = method.getAnnotation(Autowired.class);
            Qualifier qualifier = method.getAnnotation(Qualifier.class);
            String beanname = "";
            if (qualifier != null)
            {
                beanname = qualifier.value();
            }
            if (method.getName().startsWith("set"))
            {
                if (annotation != null)
                {
                    Class<?>[] types = method.getParameterTypes();
                    // only first argument is consider for the simplicity
                    if (types.length == 1)
                    {
                        method.invoke(returnBean, createBean(beanname, types[0]));
                    }
                }
            }
        }
        return returnBean;
    }

  /**
     * This method used during bean creation to resolve dependency.
     * @param beanName
     * @param clz
     * @return
     * @throws Exception
     */
    private Object createBean(String beanName, Class clz) throws Exception
    {

        if (beanName.equals("") && clz.isInterface())
        {
            // find class which implements given interface
            List<Class<Object>> found = new ArrayList<Class<Object>>();
            for (Class<Object> czz : allclasses)
            {
                if (czz.isAssignableFrom(clz))
                {
                    found.add(czz);
                }
            }
            if (found.isEmpty())
            {
                throw new Exception("No implementation found ..." + clz);
            }
            else if (found.size() > 1)
            {
 throw new Exception("More than one implemenation with no qualifier ..." + clz);
            }
            else
            {
                return getBean(getBeanName(found.get(0)));
            }
        }
        else if (beanName.equals(""))
        {
            return getBean(getBeanName(clz));
        }
        else
        {
            return getBean(beanName);
        }

    }

Now framework is ready ….

Let’s configure custom framework for the Traveler class whose class diagram as below


Code snippet 7

Traveler class declares the vehicle reference which will be injected into the Traveler class when instance of traveler class is requested to the DI framework.  
package com.ninja.vehicle;

import com.ninja.annoation.Autowired;
import com.ninja.annoation.Qualifier;
import com.ninja.annoation.Service;

/**
 * @author Ashish.Chudasama
 */
@Service("traveler")
public class Traveler {

     
      Vehicle vehicle = null;
      //setter injection for the Vehicle dependency
      @Autowired
      @Qualifier("Car")
      public void setVehicle(Vehicle vehicle) {
            this.vehicle = vehicle;
      }
      // start journey
      public void travel() {
            vehicle.move();
      }
     
}

Code snippet 8


Vehicle class depends on Tier. Vehicle class delegates its move responsibility to rotate method of wheel implementer class.
package com.ninja.vehicle;

/**
 * @author Ashish.Chudasama
 */
public interface Vehicle {
      void move();
}

Code snippet 9

Bus is a concrete implementation of Vehicle. Bus class depends on Wheel and delegates its move task to rotate method of the wheel implementer sub-classes.
/**
 * @author Ashish.Chudasama
 */
package com.ninja.vehicle;


import com.ninja.annoation.Autowired;
import com.ninja.annoation.Qualifier;
import com.ninja.annoation.Service;
import com.novice.vehicle.component.Wheel;
@Service
@Qualifier(value="Bus")
public class Bus implements Vehicle {
      //Wheel is dependency of Vehicle to move
      Wheel wheel;
     
      // setter based dependency
      @Autowired
      public void setWheel(Wheel wheel) {
            this.wheel = wheel;
      }

      @Override
      public void move() {
            System.out.println(this.getClass().getSimpleName());
            wheel.rotate();
      }
}

Code snippet 10

Car is a concrete implementation of Vehicle. Car class depends on Wheel and delegates its move task to rotate method of the wheel implementer sub-classes.
/**
 * @author Ashish.Chudasama
 */
package com.ninja.vehicle;


import com.ninja.annoation.Autowired;
import com.ninja.annoation.Service;
import com.novice.vehicle.component.Wheel;

@Service(value = "Car")
public class Car implements Vehicle {

      // Wheel is dependency of Vehicle to move
      Wheel wheel;

      // setter based dependency
      @Autowired
      public void setWheel(Wheel wheel) {
            this.wheel = wheel;
      }

      @Override
      public void move() {
            System.out.println(this.getClass().getSimpleName());
            wheel.rotate();
      }
}

Code snippet 11

A wheel class delegate is rotation function to actual Tier implementer classes to perform task of rotation.
package com.novice.vehicle.component;

import com.ninja.annoation.Autowired;
import com.ninja.annoation.Qualifier;
import com.ninja.annoation.Service;


/**
 * @author Ashish.Chudasama
 */
@Service
public class Wheel {

      //Tire is dependency of wheel class
      Tire tire=null;

      //Supply dependency using setter
      @Autowired
      @Qualifier(value="MRFTire")
      public void setTire(Tire tire) {
            this.tire = tire;
      }
     
      //function of wheel class
      public void rotate()
      {
            tire.rotate();
      }
}

Code snippet 12

Tire is an interface which ensures that all sub-classes or implementer of Tire can perform the task of rotation.
package com.novice.vehicle.component;

/**
 * @author Ashish.Chudasama
 */
public interface Tire {
      void rotate();
}

Code snippet 13

MRFTire is a concrete implementation of Tier interface.
package com.novice.vehicle.component;

import com.ninja.annoation.Service;
/**
 * @author Ashish.Chudasama
 */
@Service(value="MRFTire")
public class MRFTire implements Tire {
      @Override
      public void rotate() {
            System.out.println(this.getClass().getSimpleName());
            System.out.println("Actual Work… (rotating tire)");
      }

}

Code snippet 14

JKTire is a concrete implementation of Tier interface.
package com.novice.vehicle.component;

import com.ninja.annoation.Service;


/**
 * @author Ashish.Chudasama
 */
@Service(value="JKTire")
public class JKTire implements Tire {
      @Override
      public void rotate() {
            System.out.println(this.getClass().getSimpleName());
            System.out.println("Actual Work… (rotating tire)");
      }

}


Code snippet 15


This is test stub to check the CUSTOM dependency injection.

package org.ashish.test;

import org.ashish.di.DIAwareBeanFactory;

import com.ninja.vehicle.Traveler;

public class TestStub
{
    public static void main(String[] args) throws Exception
    {
        DIAwareBeanFactory bf = new DIAwareBeanFactory(new String[] { "com" });
        Traveler bean = (Traveler) bf.getBean("traveler");
        bean.travel();
    }

}
OUTPUT
Car
MRFTire
Actual Work… (rotating tire)


Click here to download source code.




1 comment:

  1. God am so glad there is somebody who takes so much time to help others......so very well explained.... thank you so much Ashish..... u Rock Brother..! keep up the good work

    Cheers,...!

    ReplyDelete