Write a simple example of MVC framework

Time:2019-10-6

This chapter first adds annotation support so that configuration files do not need to be modified frequently.

As for the view processing, we should use JSON first and write it later.

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

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

How to write it?

Because before I wrote the code, I divided the tasks of each class into more clearly, so when I added this function, it was relatively simple to write, and the places needed to modify were relatively small.

What we need to do in this chapter are:

  • Define an annotation, and the way to identify annotations added to a class is a UrlMethod Mapping.
  • Modify the configuration file and add the package that needs to be scanned.
  • Write a method to find all the classes according to the package median.
  • In the factory class UrlMethodMapping Factory of UrlMethodMapping, add a new method to create UrlMethodMapping based on annotations.
  • In the init () method in Application, a new factory class method is executed based on whether annotation support is turned on or not.
  • Finished.

How simple it is

Now start writing

Define an annotation Request

About how to customize the annotation, you can search it online, which is relatively simple. I’m just going to make a brief comment here. I’ll post the code first:

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

/**
 * Represents that in this class, the method with the @Request annotation is mapped to an 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
   * When added to a class, the value in value is added as the base address before the value of @Request.value() on other methods.
   *
   * @return
   */
  String value() default "/";
}

Defining an annotation requires several things:

1:@interface: Explains that this class is a comment.

2:@Retention: Retention strategy for annotations has several ranges of values:

Code Explain
@Retention(RetentionPolicy.SOURCE) Annotations exist only in source code
@Retention(RetentionPolicy.CLASS) Annotations will exist in the class bytecode file
@Retention(RetentionPolicy.RUNTIME) Annotations exist in the class bytecode file and can be retrieved by reflection at run time

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

3:@Target: Target, which indicates where annotations can be added and the range of values is:

Code Explain
@Target(ElementType.TYPE) Interfaces, classes, enumerations, annotations
@Target(ElementType.FIELD) Constants of fields and enumerations
@Target(ElementType.METHOD) Method
@Target(ElementType.PARAMETER) Method parameter
@Target(ElementType.CONSTRUCTOR) Constructor
@Target(ElementType.LOCAL_VARIABLE) local variable
@Target(ElementType.ANNOTATION_TYPE) annotation
@Target(ElementType.PACKAGE) package

3:@Documented: This is mainly to keep the custom annotations in the document, which is not practical, and is usually added.

4: default: is to give a default value to the attribute in the annotation (which looks like a method or maybe a method, but I’m just called an attribute, slightly ~~~).

The above outlines how to define an annotation. Now that the annotation is finished, let’s talk about the usefulness of this annotation.

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

Now that the annotations are finished, let’s change the configuration file.

Modify the framework’s configuration file

Just add a property. 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 annotation support value is true, the annotation is opened.

2: annotation Package represents the path of the package that needs to be scanned.

3: Because of annotation support, in order to prevent re-registration of UrlMethod Mapping, I commented out the following configuration.

Write a package scan method

This method needs to find all qualified classes in the jar files and folders of the project, and it will use recursion. The code in ClassUtils. Java consists of three methods, namely:

1:void getClassByPackage(String packageName, Set

This method takes two parameters, one is the package name packageName and the other is an empty Set (not null), which fills all classes under the package into the Set after the method is executed. The main purpose here is to determine which types of files are in this package, and to process them separately according to the file type.

Note: If it is a jar file type, the obtained filePath is as follows:

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

You need to get rid of the head and tail, and then you can eat, chicken flavor! This is what happens after the crunch ~~treatment:

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

The following is the method code:

/**
 * Find out all the classes from a given enrollment
 *
 * @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)) {
      // Path to intercept files
      filePath = filePath.substring(filePath.indexOf(":") + 1, filePath.indexOf("!"));
      getClassByJarPath(packageName, filePath, classes);
    }
  }
}

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

Find all eligible classes in the folder and use recursion. The absolute path of the class file needs to be truncated into the fully qualified name of the class. The code looks like this:

/**
 * Recursively locate the class in the package in the folder
 *
 * @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's a class file
    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);
      // Not an internal class
      boolean notInnerClass = className.indexOf("$") == -1;
      if (notInnerClass) {
        // Loading class objects based on class names
        Class aClass = ClassUtils.forName(className);
        if (aClass != null) {
          classes.add(aClass);
        }
      }
    }
  }
}

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

Find all eligible classes in the jar file. Nothing to say, here’s the code:

/**
 * Find the class in the package in the folder in the jar file
 *
 * @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);
    // Class under package
    boolean trueClass = jarEntryName.endsWith(CLASS_MARK) && jarEntryName.startsWith(packageName);
    // Not an internal class
    boolean notInnerClass = jarEntryName.indexOf("$") == -1;
    if (trueClass && notInnerClass) {
      String className = jarEntryName.substring(0, jarEntryName.length() - 6);
      System.out.println(className);
      // Loading class objects based on class names
      Class aClass = ClassUtils.forName(className);
      if (aClass != null) {
        classes.add(aClass);
      }
    }
  }
}

In this way, the class under the package name is written to

Modify UrlMethod Mapping Factory

A new method is added here:

List, which takes the Class object obtained after scanning the package as a parameter and returns a collection of UrlMethod Mapping. The code is as follows:

/**
 * Getting mappings by parsing Class
 *
 * @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;
    }
    // Use the path added to Request on the class as the underlying path
    String url = UrlUtils.makeUrl(basePath + "/" + urlMethodMapping.getUrl());
    urlMethodMapping.setUrl(url);
    mappings.add(urlMethodMapping);
  }
  return mappings;
}

/**
 * Getting mappings by parsing methods
 * Jump out when annotation Request does not 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() +" exception of request path: "+ path +"! "";
  }
  return getUrlMethodMapping(
      path,
      request.type(),
      declaringClass,
      method,
      method.getParameterTypes()
  );
}

The value of the value in the annotated Request is checked here, and an exception is thrown if there is a space in the middle. UrlUtils. makeUrl () This method is mainly to remove the redundant “/” in the url, the code looks like this:

private static final String SLASH = "/";

/**
 * processing URL
 * 1: Remove the adjacent and repeated "/" in the connection.
 * 2: If there is no "/" at the beginning of the link, add it.
 * 3: If there is "/" at the end of the link, remove it.
 *
 * @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();
}

This completes the factory method of obtaining UrlMethodMapping through annotations, and the next step is to modify the code of the loading framework.

Modify init in Application

A new method is created here because of the addition of a method to obtain UrlMethod Mapping using annotations:

Void addApplication Url Mapping ByAnnotation Config (JSONObject config Json). Here, get the package name in the framework configuration and do some configuration checks. The code is as follows:

/**
 * Use annotations to load UrlMethod Mapping
 *
 * @param configJson
 */
private void addApplicationUrlMappingByAnnotationConfig(JSONObject configJson) {
  String annotationPackage = configJson.getString(ANNOTATION_PACKAGE_NODE);
  Assert.notNull(annotationPackage, ANNOTATION_PACKAGE_NODE + NOT_FIND);
  // Get the class with @Request added
  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);
    }
  }
}

Then extract the code that read JSON configuration to generate urlMappin and write a method separately:

Void addApplication UrlMapping ByJson Config (JSONObject configJson) makes each method in the code function independent and looks neat and clear. The code is as follows:

/**
 * Use file configuration to load UrlMethod Mapping
 * 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’s 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: Factory class that generates objects (default is to have a new object every time)
  this.objectFactory = new AlwaysNewObjectFactory();
  // TODO: Different entry name acquisition classes (currently defaulted to asm)
  urlMethodMappingFactory.setParamNameGetter(new AsmParamNameGetter());
  // Loading through file configuration
  addApplicationUrlMappingByJsonConfig(configJson);
  // Whether to turn on annotation support
  Boolean annotationSupport = configJson.getBoolean(ANNOTATION_SUPPORT_NODE);
  Assert.notNull(annotationSupport, ANNOTATION_SUPPORT_NODE + NOT_FIND);
  if (annotationSupport) {
    addApplicationUrlMappingByAnnotationConfig(configJson);
  }
}