曹工說面試:當應用依賴jar包的A版本,中間件jar包依賴B版本,兩個版本不兼容,這還怎么玩?


背景

大一點的公司,可能有一些組,專門做中間件的;假設,某中間件小組,給你提供了一個jar包,你需要集成到你的應用里。假設,它依賴了一個日期類,版本是v1;我們應用也依賴了同名的一個日期類,版本是v2.

兩個版本的日期類,方法邏輯的實現,有一些差異。

舉個例子,中間件提供的jar包中,依賴如下工具包:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>common-v1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

該版本中,包含了com.example.date.util.CommonDateUtil這個類。

package com.example.date.util;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CommonDateUtil {

    public static String format(String date) {
        // 1
        String s = date + "- v1";
        log.info("v1 result:{}", s);
        return s;
    }
}

應用中,依賴如下jar包:

<dependency>
   <groupId>com.example</groupId>
   <artifactId>common-v2</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</dependency>

該jar包中,包含同名的class,但里面的方法實現不一樣:

@Slf4j
public class CommonDateUtil {

    public static String format(String date) {
        String s = date + "- v2";
        log.info("v2 result:{}", s);
        return s;
    }
}

ok,那假設我們是一個spring boot應用,當中間件小組的哥們找到你,讓你集成,你可能就愉快地弄進去了;但是,有個問題時,你的jar包、和中間件小哥的jar包,都是放在fatjar的lib目錄的(我這里解壓了,方便查看):

請問,最終加載的CommonDateUtil類,到底是common-v1的,還是commonv2中的呢?因為spring boot加載BOOT-INF/lib時,肯定都是同一個類加載器,同一個類加載器,對於一個包名和類名都相同的類,只會加載一次;那么,加載了v1,就不可能再加載V2;反之亦然。

那這就出問題了。我們應用要用V2;中間件要用V1,水火不容啊,這可怎么辦?

分析

首先,我們要重寫spring boot的啟動類,這是毋庸置疑的,啟動類是哪個呢?

為什么要重寫這個?因為,我們問題分析里說了,當打成fat jar運行時,其結構如下:

[root@mini2 temp]# tree 
.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   ├── application.yml
│   │   └── com
│   │       └── example
│   │           └── demo
│   │               ├── CustomMiddleWareClassloader.class
│   │               └── OrderServiceApplication.class
│   └── lib
│       ├── classmate-1.4.0.jar
│       ├── common-v1-0.0.1-SNAPSHOT.jar
│       ├── common-v2-0.0.1-SNAPSHOT.jar
│       ├── hibernate-validator-6.0.17.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.9.jar
│       ├── jackson-databind-2.9.9.jar
│       ├── jackson-datatype-jdk8-2.9.9.jar
│       ├── jackson-datatype-jsr310-2.9.9.jar
│       ├── jackson-module-parameter-names-2.9.9.jar
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jboss-logging-3.3.2.Final.jar
│       ├── jul-to-slf4j-1.7.26.jar
│       ├── log4j-api-2.11.2.jar
│       ├── log4j-to-slf4j-2.11.2.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── lombok-1.18.10.jar
│       ├── middle-ware-0.0.1-SNAPSHOT.jar
│       ├── middle-ware-api-0.0.1-SNAPSHOT.jar
│       ├── slf4j-api-1.7.26.jar
│       ├── snakeyaml-1.23.jar
│       ├── spring-aop-5.1.9.RELEASE.jar
│       ├── spring-beans-5.1.9.RELEASE.jar
│       ├── spring-boot-2.1.7.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.1.7.RELEASE.jar
│       ├── spring-boot-loader-2.1.7.RELEASE.jar
│       ├── spring-boot-starter-2.1.7.RELEASE.jar
│       ├── spring-boot-starter-json-2.1.7.RELEASE.jar
│       ├── spring-boot-starter-logging-2.1.7.RELEASE.jar
│       ├── spring-boot-starter-tomcat-2.1.7.RELEASE.jar
│       ├── spring-boot-starter-web-2.1.7.RELEASE.jar
│       ├── spring-context-5.1.9.RELEASE.jar
│       ├── spring-core-5.1.9.RELEASE.jar
│       ├── spring-expression-5.1.9.RELEASE.jar
│       ├── spring-jcl-5.1.9.RELEASE.jar
│       ├── spring-web-5.1.9.RELEASE.jar
│       ├── spring-webmvc-5.1.9.RELEASE.jar
│       ├── tomcat-embed-core-9.0.22.jar
│       ├── tomcat-embed-el-9.0.22.jar
│       ├── tomcat-embed-websocket-9.0.22.jar
│       └── validation-api-2.0.1.Final.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.example
│           └── web-application
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── archive
                │   ├── Archive.class
                │   ├── Archive$Entry.class
                │   ├── Archive$EntryFilter.class
                │   ├── ExplodedArchive$1.class
                │   ├── ExplodedArchive.class
                │   ├── ExplodedArchive$FileEntry.class
                │   ├── ExplodedArchive$FileEntryIterator.class
                │   ├── ExplodedArchive$FileEntryIterator$EntryComparator.class
                │   ├── JarFileArchive.class
                │   ├── JarFileArchive$EntryIterator.class
                │   └── JarFileArchive$JarFileEntry.class
                ├── data
                │   ├── RandomAccessData.class
                │   ├── RandomAccessDataFile$1.class
                │   ├── RandomAccessDataFile.class
                │   ├── RandomAccessDataFile$DataInputStream.class
                │   └── RandomAccessDataFile$FileAccess.class
                ├── ExecutableArchiveLauncher.class
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── Bytes.class
                │   ├── CentralDirectoryEndRecord.class
                │   ├── CentralDirectoryFileHeader.class
                │   ├── CentralDirectoryParser.class
                │   ├── CentralDirectoryVisitor.class
                │   ├── FileHeader.class
                │   ├── Handler.class
                │   ├── JarEntry.class
                │   ├── JarEntryFilter.class
                │   ├── JarFile$1.class
                │   ├── JarFile$2.class
                │   ├── JarFile.class
                │   ├── JarFileEntries$1.class
                │   ├── JarFileEntries.class
                │   ├── JarFileEntries$EntryIterator.class
                │   ├── JarFile$JarFileType.class
                │   ├── JarURLConnection$1.class
                │   ├── JarURLConnection.class
                │   ├── JarURLConnection$JarEntryName.class
                │   ├── StringSequence.class
                │   └── ZipInflaterInputStream.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── Launcher.class
                ├── MainMethodRunner.class
                ├── PropertiesLauncher$1.class
                ├── PropertiesLauncher$ArchiveEntryFilter.class
                ├── PropertiesLauncher.class
                ├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
                ├── util
                │   └── SystemPropertyUtils.class
                └── WarLauncher.class

BOOT-INF/lib下,是由同一個類加載器去加載的,而我們的V1和V2的jar包,全部混在這個目錄下。

我們要想同時加載V1和V2的jar包,必須用兩個類加載器來做隔離。

即,應用類加載器,不能加載V1;而中間件類加載器,只管加載V2,其他一概不能加載。

spring boot 的啟動類

前面我們提到,啟動類是org.springframework.boot.loader.JarLauncher,這個是在BOOT-INF/MANIFEST中指定了的。

這個類在哪里呢,一般在如下這個依賴中:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-loader</artifactId>
</dependency>

該依賴,會在打包階段,由maven插件,打到我們的fat jar中:

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

這個jar包,打到哪里去了呢?實際是解壓后,放到fat jar的如下路徑了,可以再去上面看看那個fat jar結構:

上面那個啟動類,就是在這個里面。

啟動類簡單解析

先來看看uml:

先看看JarLauncher:

public class JarLauncher extends ExecutableArchiveLauncher {

   static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

   static final String BOOT_INF_LIB = "BOOT-INF/lib/";

   public JarLauncher() {
   }

   protected JarLauncher(Archive archive) {
      super(archive);
   }

   @Override
   protected boolean isNestedArchive(Archive.Entry entry) {
      if (entry.isDirectory()) {
         return entry.getName().equals(BOOT_INF_CLASSES);
      }
      return entry.getName().startsWith(BOOT_INF_LIB);
   }

   public static void main(String[] args) throws Exception {
      // 1
      new JarLauncher().launch(args);
   }

}

這里1處,new了自身,然后調用launch。

public abstract class Launcher {

   /**
    * Launch the application. This method is the initial entry point that should be
    * called by a subclass {@code public static void main(String[] args)} method.
    * @param args the incoming arguments
    * @throws Exception if the application fails to launch
    */
   protected void launch(String[] args) throws Exception {
      JarFile.registerUrlProtocolHandler();
      // 1
      ClassLoader classLoader = createClassLoader(getClassPathArchives());
      // 2
      launch(args, getMainClass(), classLoader);
   }

1處這里,就是創建類加載器了,期間,先調用了getClassPathArchives。

我們看看:

org.springframework.boot.loader.Launcher#getClassPathArchives
protected abstract List<Archive> getClassPathArchives() throws Exception;

這是個抽象方法,此處使用了模板方法設計模式,在如下類中實現了:

org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives
    
@Override
protected List<Archive> getClassPathArchives() throws Exception {
    // 1
   List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
    
   postProcessClassPathArchives(archives);
   return archives;
}

此處的1處,不用深究,就是獲取類加載器的classpath集合。我們這里打個斷點,直接看下:

這里面細節就先不看了,主要就是拿到BOOT-INF/lib下的每個jar包。

然后我們繼續之前的:

   protected void launch(String[] args) throws Exception {
      JarFile.registerUrlProtocolHandler();
      // 1
      ClassLoader classLoader = createClassLoader(getClassPathArchives());
      // 2
      launch(args, getMainClass(), classLoader);
   }

現在getClassPathArchives已經ok了,接着就調用createClassLoader來創建類加載器了。

	protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
		List<URL> urls = new ArrayList<>(archives.size());
		for (Archive archive : archives) {
			urls.add(archive.getUrl());
		}
        // 1
		return createClassLoader(urls.toArray(new URL[0]));
	}

1處,繼續調用內層函數,傳入了url數組。

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
   return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

這里new了一個LaunchedURLClassLoader,參數就是url數組。我們看看這個類:

public class LaunchedURLClassLoader extends URLClassLoader {

   static {
      ClassLoader.registerAsParallelCapable();
   }

   /**
    * Create a new {@link LaunchedURLClassLoader} instance.
    * @param urls the URLs from which to load classes and resources
    * @param parent the parent class loader for delegation
    */
   public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
      super(urls, parent);
   }

這個類,繼承了URLClassLoader,所以,大家如果對類加載器有一定了解,就知道,URLClassLoader就是接收一堆的url,然后loadClass的時候,遵從雙親委派,雙親加載不了,就交給它,它就去url數組里,去加載class。

思路分析

我的打算是,修改fat jar中的啟動類,為我們自定義的啟動類。

Manifest-Version: 1.0
Implementation-Title: web-application
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.demo.OrderServiceApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.7.RELEASE
Created-By: Maven Archiver 3.4.0
// 1
Main-Class: com.example.demo.CustomJarLauncher

1處,指定我們自定義的class,這個class,我們會在打好fat jar后,手動拷貝進去。

然后,我們自定義啟動類里面要干啥呢?

public class CustomJarLauncher extends JarLauncher {



    public static void main(String[] args) throws Exception {
        new CustomJarLauncher().launch(args);
    }


    @Override
    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        // 1
        List<Archive> classPathArchives = getClassPathArchives();
        /**
         * 2
         */
        List<URL> allURLs = classPathArchives.stream().map(entries -> {
            try {
                return entries.getUrl();
            } catch (MalformedURLException e) {
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());

		// 3
        List<URL> middleWareClassPathArchives = new ArrayList<>();
        for (URL url : allURLs) {
            String urlPath = url.getPath();
            if (urlPath == null) {
                continue;
            }
            boolean isMiddleWareJar = urlPath.contains("common-v1")
                    || urlPath.contains("middle-ware");
            if (isMiddleWareJar) {
                if (urlPath.contains("middle-ware-api")) {
                    continue;
                }
                middleWareClassPathArchives.add(url);
            }
        }


        /**
         * 4 從全部的應用lib目錄,移除中間件需要的jar包,但是,中間件的api不能移除
         */
        allURLs.removeAll(middleWareClassPathArchives);

		// 5
        CustomLaunchedURLClassLoader loader =
                new CustomLaunchedURLClassLoader(allURLs.toArray(new URL[0]),
                getClass().getClassLoader());
        loader.setMiddleWareClassPathArchives(middleWareClassPathArchives);

        launch(args, getMainClass(), loader);
    }

}
  • 1處,獲取fat jar的lib目錄下的全部包
  • 2處,將1處得到的集合,轉為url集合
  • 3處,篩選出中間件的包,復制到單獨的集合中,我這邊有2個,直接寫死了(畢竟是demo)
  • 4處,將原集合中,移除中間件的jar包
  • 5處,創建一個自定義的classloader,主要是方便我們存放中間件相關jar包集合

5處自定義的classloader,這里可以看下:

public class CustomLaunchedURLClassLoader extends LaunchedURLClassLoader {
	// 中間件jar包
	List<URL> middleWareClassPathArchives;

	/**
	 * Create a new {@link LaunchedURLClassLoader} instance.
	 *
	 * @param urls   the URLs from which to load classes and resources
	 * @param parent the parent class loader for delegation
	 */
	public CustomLaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
		super(urls, parent);
	}


	public List<URL> getMiddleWareClassPathArchives() {
		return middleWareClassPathArchives;
	}

	public void setMiddleWareClassPathArchives(List<URL> middleWareClassPathArchives) {
		this.middleWareClassPathArchives = middleWareClassPathArchives;
	}
}

最終,在我們的業務代碼,要怎么去寫呢?

我們現在自定義了一個類加載器,那么,后續業務代碼都會由這個類加載器去加載。

我們再想想標題說的問題,我們是需要:加載中間件代碼時,不能用這個類加載器去加載,因為這個類加載器中,已經排除了中間件相關jar包,是加載不到的。

此時,我們需要自定義一個classloader,去如下類中的middleWareClassPathArchives這個地方加載:

public class CustomLaunchedURLClassLoader extends LaunchedURLClassLoader {
	// 中間件jar包
	List<URL> middleWareClassPathArchives;
    ...
}

只有當它加載不到之后,才丟給應用類加載器去加載。

代碼如下:

@SpringBootApplication
@RestController
@Slf4j
public class OrderServiceApplication {
	/**
	 * 中間件使用的classloader
	 */
	static ClassLoader delegatingClassloader;

	public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 1
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Method method = loader.getClass().getMethod("getMiddleWareClassPathArchives");
		List<URL> middleWareUrls = (List<URL>) method.invoke(loader);
		// 2
		delegatingClassloader = new CustomMiddleWareClassloader(middleWareUrls.toArray(new URL[0]), loader);
		// 3
		SpringApplication.run(OrderServiceApplication.class, args);
	}
  • 1,這里,我們要通過當前線程,拿到我們的類加載器,此時拿到的,肯定就是我們的自定義類加載器;然后通過反射方法,拿到中間件url集合,其實這里自己去拼這個url也可以,我們這里為了省事,所以就這么寫;

  • 2處,創建一個類加載器,主要就是給中間件代碼使用,進行類加載器隔離。

    注意,這里,我們把當前應用的類加載器,傳給了這個中間件類加載器。

@Data
@Slf4j
public class CustomMiddleWareClassloader extends URLClassLoader {
    ClassLoader classLoader;

    public CustomMiddleWareClassloader(URL[] urls, ClassLoader parent) {
        super(urls);
        classLoader = parent;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        /**
         * 先自己來加載中間件相關jar包,這里調用findClass,就會去中間件那幾個jar包加載class
         */
        try {
            Class<?> clazz = findClass(name);
            if (clazz != null) {
                return clazz;
            }
            throw new ClassNotFoundException(name);
        } catch (Exception e) {
            /**
             * 在中間件自己的jar包里找不到,就交給自己的parent,此處即應用類加載器
             */
            return classLoader.loadClass(name);
        }
    }


}

代碼結構

中間件整體模塊概覽

在繼續之前,有必要說下代碼結構。

中間件總共三個jar包:

common-v1,middle-ware,middle-ware-api。

其中,middle-ware的pom如下:


中間件api模塊

該模塊無任何依賴,就是個接口

public interface IGisUtilInterface {

    String getFormattedDate(String date);

}

該模塊是很有必要的,該api模塊必須由應用的類加載器加載,沒錯,是應用類加載器。

類似於servlet-api吧。

大家可以暫時這么記着,至於原因,那就有點長了。

可以參考:

不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到

中間件實現模塊

實現模塊的pom:

<groupId>com.example</groupId>
<artifactId>middle-ware</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>middle-ware</name>
    
<dependencies>
   <dependency>
      <groupId>com.example</groupId>
      <artifactId>middle-ware-api</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </dependency>
   <dependency>
      <groupId>com.example</groupId>
      <artifactId>common-v1</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </dependency>

</dependencies>

里面只有一個類,就是實現api模塊的接口。

@Slf4j
public class GisUtilImpl implements IGisUtilInterface{

    @Override
    public  String getFormattedDate(String date) {
        String v1 = CommonDateUtil.format(date);
        log.info("invoke common v1,get result:{}", v1);

        return v1;
    }

}

spring boot 的自定義loader模塊

這部分和業務關系不大,主要就是自定義我們前面的那個fat jar啟動類。

<groupId>com.example</groupId>
<artifactId>custom-jar-launch</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>custom-jar-launch</name>
<description>Demo project for Spring Boot</description>

<properties>
   <java.version>1.8</java.version>
   <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>

<dependencies>

   <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.10</version>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-loader</artifactId>
   </dependency>

</dependencies>

這里有個特別的依賴:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-loader</artifactId>
</dependency>

該模塊,主要包含:

com.example.demo.CustomJarLauncher

com.example.demo.CustomLaunchedURLClassLoader

應用程序

<groupId>com.example</groupId>
<artifactId>web-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-application</name>
<description>Demo project for Spring Boot</description>


<dependencies>
   <dependency>
      <groupId>com.example</groupId>
      <artifactId>common-v2</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </dependency>

   <dependency>
      <groupId>com.example</groupId>
      <artifactId>middle-ware-api</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </dependency>
   <dependency>
      <groupId>com.example</groupId>
      <artifactId>middle-ware</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </dependency>

模擬jar包沖突場景,此時,我們已經同時依賴了v1和v2了。

其測試代碼如下:

public class OrderServiceApplication {
	/**
	 * 中間件使用的classloader
	 */
	static ClassLoader delegatingClassloader;
    
@RequestMapping("/")
public  void test() throws ClassNotFoundException, IllegalAccessException {
   // 1
   Class<?> middleWareImplClass = delegatingClassloader.loadClass("com.example.demo.GisUtilImpl");
    // 2
   IGisUtilInterface iGisUtilInterface = (IGisUtilInterface) middleWareImplClass.newInstance();
    // 3
   String middleWareResult = iGisUtilInterface.getFormattedDate("version:");

   log.info("middle ware result:{}",middleWareResult);
	
    // 4
   String result = CommonDateUtil.format("version:");
   log.info("application result:{}",result);

}
  • 1處,類似於servlet,也是把servlet實現類寫死在web.xml的,我們這里也一樣,把中間件的實現類寫死了。

    可能有更好的方式,暫時先這樣。

  • 2處,將實現類(中間件類加載器加載),轉換為接口類(應用類加載器加載)。之所以要定義接口,這里很關鍵。

    可以再仔細看看:

    不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到

  • 3處,調用中間件代碼

  • 4處,調用應用代碼

效果展示

2020-05-22 06:37:13.481  INFO 6676 --- [nio-8082-exec-1] com.example.demo.GisUtilImpl             : invoke common v1,get result:version:- v1
2020-05-22 06:37:13.481  INFO 6676 --- [nio-8082-exec-1] c.example.demo.OrderServiceApplication   : middle ware result:version:- v1
2020-05-22 06:37:13.482  INFO 6676 --- [nio-8082-exec-1] com.example.date.util.CommonDateUtil     : v2 result:version:- v2
2020-05-22 06:37:13.482  INFO 6676 --- [nio-8082-exec-1] c.example.demo.OrderServiceApplication   : application result:version:- v2

可以發現,中間件那里,是v1;而調用應用的方法,則是v2。

說明我們成功了。

我這里用arthas分析了下這個類:

這個類,還在下面出現:

這個是中間件加載的。

所以,大家平安無事地繼續生活在了一起。

源碼

https://gitee.com/ckl111/all-simple-demo-in-work-1/tree/master/jar-conflict

該源碼怎么使用?

先正常打包應用為fat jar,然后將custom-jar-launch中的class,拷進fat jar,然后修改META-INF/MANIFEST文件的啟動類。

然后調用接口:

 curl  localhost:8082

總結

希望對大家有所啟發,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM