Write a simple example of mvc framework

  • 2021-11-01 03:01:03
  • OfStack

In this chapter 1, the function of supporting annotations is added first, so that there is no need to modify the configuration file frequently.

As for the view processing place, let's use json first, and then write it sometime.

The address of the project is: https://github.com/hjx601496320/aMvc.

The test code is at: https://github.com/hjx601496320/amvc-test.

How to write it?

Because before writing the code, I put each class to do things more clearly, so when adding this function to write up is relatively simple, the need to modify the place is relatively small.

The things we need to do in this chapter are:

Defines an annotation, and identifies that the annotated method in an class is an UrlMethodMapping. Modify the configuration file and add package to be scanned. Write a method to find all class based on the values in package. Add a new method to the factory class UrlMethodMappingFactory of UrlMethodMapping to create UrlMethodMapping according to annotations. In the init () method in Application, a new factory class method is executed depending on whether annotation support is turned on or not. It's over.

How simple it is ~ ~ ~

Start writing now

Define 1 annotation Request

About how to customize note this matter, you can search 1 on the Internet, which is relatively simple. I'm just going to say it briefly here. I'll post the code first:


import com.hebaibai.amvc.RequestType;
import java.lang.annotation.*;

/**
 *  Indicates that in this class, the @Request Annotated method Is mapped to 1 A http Address. 
 *
 * @author hjx
 */
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Request {

  /**
   *  Request type 
   *  Support GET , POST , DELETE , PUT
   *
   * @return
   */
  RequestType[] type() default {RequestType.GET, RequestType.POST, RequestType.DELETE, RequestType.PUT};

  /**
   *  Request address 
   *  Add in the class Will set the value To add the value in the other method @Request.value() As the base address before the value of. 
   *
   * @return
   */
  String value() default "/";
}

To define an annotation, you need to use the following things:

1: @ interface: Explains that this class is an annotation.

2: @ Retention: The annotation retention policy has several value ranges:

代码 说明
@Retention(RetentionPolicy.SOURCE) 注解仅存在于源码中
@Retention(RetentionPolicy.CLASS) 注解会在class字节码文件中存在
@Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,运行时可以通过反射获取到

Because we need to get custom annotations in the program, we use: RetentionPolicy. RUNTIME.

3: @ Target: Action target, indicating where annotations can be added. The value range is:

代码 说明
@Target(ElementType.TYPE) 接口、类、枚举、注解
@Target(ElementType.FIELD) 字段、枚举的常量
@Target(ElementType.METHOD) 方法
@Target(ElementType.PARAMETER) 方法参数
@Target(ElementType.CONSTRUCTOR) 构造函数
@Target(ElementType.LOCAL_VARIABLE) 局部变量
@Target(ElementType.ANNOTATION_TYPE) 注解
@Target(ElementType.PACKAGE)

3: @ Documented: This is mainly to keep custom comments in the document, which has no practical significance.

4: default: Is a default value for the attribute in the annotation (it looks like a method, or it may be a method, but I just call it an attribute, just a little ~ ~ ~).

The above is roughly about how to define a annotation. Now that the annotation is finished, let's talk about the use of this annotation.

First, this annotation can be added to class and method. When added to class, it means that there will be an method in this class that will be processed as an UrlMethodMapping, and then the value attribute will be the base address of all UrlMethodMapping in this class, and the type attribute will not work. When added to method, it means that this method will be processed into an UrlMethodMapping, and the two attributes of annotations play their normal role.

After writing the annotations, change the configuration file to 1 below.

Modify the configuration file of the framework

Just add one attribute, and the modified configuration file looks like this:


{
 "annotationSupport": true,
 "annotationPackage": "com.hebaibai.demo.web",
// "mapping": [
//  {
//   "url": "/index",
//   "requestType": [
//    "get"
//   ],
//   "method": "index",
//   "objectClass": "com.hebaibai.demo.web.IndexController",
//   "paramTypes": [
//    "java.lang.String",
//    "int"
//   ]
//  }
// ]
}

1: When the value of annotationSupport is true, it indicates that the annotation is opened.

2: annotationPackage indicates the path of the packet to be scanned.

3: Because of annotation support, in order to prevent repeated registration of UrlMethodMapping, I commented out the following configuration.

Method of writing 1 packet scan

This method needs to find all qualified class under jar files and folders in the project, and recursion will be used. The code is in ClassUtils. java, which consists of three methods, namely:

1: void getClassByPackage (String packageName, Set)

This method takes two parameters, one is the package name packageName, one is an empty Set (not null), and all class under the package are populated into Set at the end of the method execution. Here is mainly to judge the 1 under the package which types of files, and according to the file types of processing.

Note: If it is the type of jar file, the resulting filePath looks like this:

file:/home/hjx/idea-IU/lib/idea_rt.jar!/com

Need to turn around and tail, and then you can eat it, chicken flavor! Crunchy ~ ~ After treatment, it looks like this:

/home/hjx/idea-IU/lib/idea_rt.jar

Here's the method code:


/**
 *  Find out all of the given entries class
 *
 * @param packageName
 * @param classes
 */
@SneakyThrows({IOException.class})
public static void getClassByPackage(String packageName, Set<Class> classes) {
  Assert.notNull(classes);
  String packagePath = packageName.replace(DOT, SLASH);
  Enumeration<URL> resources = ClassUtils.getClassLoader().getResources(packagePath);
  while (resources.hasMoreElements()) {
    URL url = resources.nextElement();
    // File type 
    String protocol = url.getProtocol();
    String filePath = URLDecoder.decode(url.getFile(), CHARSET_UTF_8);
    if (TYPE_FILE.equals(protocol)) {
      getClassByFilePath(packageName, filePath, classes);
    }
    if (TYPE_JAR.equals(protocol)) {
      // Intercept the path of the file 
      filePath = filePath.substring(filePath.indexOf(":") + 1, filePath.indexOf("!"));
      getClassByJarPath(packageName, filePath, classes);
    }
  }
}

2: void getClassByFilePath (String packageName, String filePath, Set

Find all eligible class in the folder and use recursion. You need to intercept the absolute path of the class file into the fully qualified name of class. The code looks like this:


/**
 *  Recursively in a folder to find the package In class
 *
 * @param packageName
 * @param filePath
 * @param classes
 */
static void getClassByFilePath(
  String packageName,
  String filePath,
  Set<Class> classes
) {
  File targetFile = new File(filePath);
  if (!targetFile.exists()) {
    return;
  }
  if (targetFile.isDirectory()) {
    File[] files = targetFile.listFiles();
    for (File file : files) {
      String path = file.getPath();
      getClassByFilePath(packageName, path, classes);
    }
  } else {
    // If it is 1 A class Documents 
    boolean trueClass = filePath.endsWith(CLASS_MARK);
    if (trueClass) {
      // Extract the complete class name 
      filePath = filePath.replace(SLASH, DOT);
      int i = filePath.indexOf(packageName);
      String className = filePath.substring(i, filePath.length() - 6);
      // No 1 Inner classes 
      boolean notInnerClass = className.indexOf("$") == -1;
      if (notInnerClass) {
        // Load based on class name class Object 
        Class aClass = ClassUtils.forName(className);
        if (aClass != null) {
          classes.add(aClass);
        }
      }
    }
  }
}

3: void getClassByJarPath (String packageName, String filePath, Set

Find all eligible class in the jar file. Nothing to say, here is the code:


/**
 *  In jar File to find out the file in the folder package In class
 *
 * @param packageName
 * @param filePath
 * @param classes
 */
@SneakyThrows({IOException.class})
static void getClassByJarPath(
  String packageName,
  String filePath,
  Set<Class> classes
) {
  JarFile jarFile = new URLJarFile(new File(filePath));
  Enumeration<JarEntry> entries = jarFile.entries();
  while (entries.hasMoreElements()) {
    JarEntry jarEntry = entries.nextElement();
    String jarEntryName = jarEntry.getName().replace(SLASH, DOT);
    // In package Under class
    boolean trueClass = jarEntryName.endsWith(CLASS_MARK) && jarEntryName.startsWith(packageName);
    // No 1 Inner classes 
    boolean notInnerClass = jarEntryName.indexOf("$") == -1;
    if (trueClass && notInnerClass) {
      String className = jarEntryName.substring(0, jarEntryName.length() - 6);
      System.out.println(className);
      // Load based on class name class Object 
      Class aClass = ClassUtils.forName(className);
      if (aClass != null) {
        classes.add(aClass);
      }
    }
  }
}

In this way, the class under the name of the package is finished ~

Modification of UrlMethodMappingFactory

A new method is added here:

List, take the Class object obtained after scanning the package as a parameter, and return a collection of UrlMethodMapping. The code is as follows:


/**
 *  By parsing Class  Get mapping 
 *
 * @param aClass
 * @return
 */
public List<UrlMethodMapping> getUrlMethodMappingListByClass(Class<Request> aClass) {
  List<UrlMethodMapping> mappings = new ArrayList<>();
  Request request = aClass.getDeclaredAnnotation(Request.class);
  if (request == null) {
    return mappings;
  }
  String basePath = request.value();
  for (Method classMethod : aClass.getDeclaredMethods()) {
    UrlMethodMapping urlMethodMapping = getUrlMethodMappingListByMethod(classMethod);
    if (urlMethodMapping == null) {
      continue;
    }
    // Will be added in the class Above Request In path As the base path 
    String url = UrlUtils.makeUrl(basePath + "/" + urlMethodMapping.getUrl());
    urlMethodMapping.setUrl(url);
    mappings.add(urlMethodMapping);
  }
  return mappings;
}

/**
 *  By parsing Method  Get mapping 
 *  Annotation Request Jump out when it doesn't exist 
 *
 * @param method
 * @return
 */
private UrlMethodMapping getUrlMethodMappingListByMethod(Method method) {
  Request request = method.getDeclaredAnnotation(Request.class);
  if (request == null) {
    return null;
  }
  Class<?> declaringClass = method.getDeclaringClass();
  String path = request.value();
  for (char c : path.toCharArray()) {
    Assert.isTrue(c != ' ', declaringClass + "." + method.getName() + " Request path exception: " + path + "  ! ");
  }
  return getUrlMethodMapping(
      path,
      request.type(),
      declaringClass,
      method,
      method.getParameterTypes()
  );
}

Here, the value of value in the annotation Request under 1 is verified, and an exception will be thrown if there is a space in the middle. UrlUtils. makeUrl () This method is mainly to remove the redundant "/" in url, and the code is as long as this:


private static final String SLASH = "/";

/**
 *  Deal with url
 * 1 : Remove adjacent and duplicate "in the connection / " ,
 * 2 : There is no "at the beginning of the link" / " , Is added. 
 * 3 Link ends with " / ", then remove. 
 *
 * @param url
 * @return
 */
public static String makeUrl(@NonNull String url) {
  char[] chars = url.toCharArray();
  StringBuilder newUrl = new StringBuilder();
  if (!url.startsWith(SLASH)) {
    newUrl.append(SLASH);
  }
  for (int i = 0; i < chars.length; i++) {
    if (i != 0 && chars[i] == chars[i - 1] && chars[i] == '/') {
      continue;
    }
    if (i == chars.length - 1 && chars[i] == '/') {
      continue;
    }
    newUrl.append(chars[i]);
  }
  return newUrl.toString();
}

With this factory method for annotating UrlMethodMapping finished, let's start modifying the code for loading the framework.

Modify init in Application

Here, because a method to get UrlMethodMapping by annotation has been added, a new method has been created:

void addApplicationUrlMappingByAnnotationConfig (JSONObject configJson). Here, get the package name in the framework configuration and do some configuration verification. The code is as follows:


/**
 *  Use annotations to load UrlMethodMapping
 *
 * @param configJson
 */
private void addApplicationUrlMappingByAnnotationConfig(JSONObject configJson) {
  String annotationPackage = configJson.getString(ANNOTATION_PACKAGE_NODE);
  Assert.notNull(annotationPackage, ANNOTATION_PACKAGE_NODE + NOT_FIND);
  // Gets the added @Request Class of 
  Set<Class> classes = new HashSet<>();
  ClassUtils.getClassByPackage(annotationPackage, classes);
  Iterator<Class> iterator = classes.iterator();
  while (iterator.hasNext()) {
    Class aClass = iterator.next();
    List<UrlMethodMapping> mappings = urlMethodMappingFactory.getUrlMethodMappingListByClass(aClass);
    if (mappings.size() == 0) {
      continue;
    }
    for (UrlMethodMapping mapping : mappings) {
      addApplicationUrlMapping(mapping);
    }
  }
}

After that, take out the code written earlier to read json configuration and generate urlMappin, and write a method separately:

void addApplicationUrlMappingByJsonConfig (JSONObject configJson), so that the function of each method in the code is independent, and it looks neat and clear. The code is as follows:


/**
 *  Use file configuration to load UrlMethodMapping
 *  If it is not found in the configuration, it will not be executed. 
 *
 * @param configJson
 */
private void addApplicationUrlMappingByJsonConfig(JSONObject configJson) {
  JSONArray jsonArray = configJson.getJSONArray(MAPPING_NODE);
  if (jsonArray == null || jsonArray.size() == 0) {
    return;
  }
  for (int i = 0; i < jsonArray.size(); i++) {
    UrlMethodMapping mapping = urlMethodMappingFactory.getUrlMethodMappingByJson(jsonArray.getJSONObject(i));
    addApplicationUrlMapping(mapping);
  }
}

Finally, just modify init () a little bit. After the modification, it looks like this:


/**
 *  Initialize configuration 
 */
@SneakyThrows(IOException.class)
protected void init() {
  String configFileName = applicationName + ".json";
  InputStream inputStream = ClassUtils.getClassLoader().getResourceAsStream(configFileName);
  byte[] bytes = new byte[inputStream.available()];
  inputStream.read(bytes);
  String config = new String(bytes, "utf-8");
  // Application configuration 
  JSONObject configJson = JSONObject.parseObject(config);

  //TODO: The factory class that generates the object (first defaults to every time new1 New objects) 
  this.objectFactory = new AlwaysNewObjectFactory();
  //TODO: Different entry parameter names get classes (currently defaults to asm ) 
  urlMethodMappingFactory.setParamNameGetter(new AsmParamNameGetter());
  // Load through file configuration 
  addApplicationUrlMappingByJsonConfig(configJson);
  // Do you want to turn on annotation support 
  Boolean annotationSupport = configJson.getBoolean(ANNOTATION_SUPPORT_NODE);
  Assert.notNull(annotationSupport, ANNOTATION_SUPPORT_NODE + NOT_FIND);
  if (annotationSupport) {
    addApplicationUrlMappingByAnnotationConfig(configJson);
  }
}


Related articles: