Multilingual exception handling usage guide

8-9 initial analysis of multilingual exception architecture
This article will explain in the order of program operation, and the methods involved will be explained.
At the end, all usage will be sorted out, and the details of possible errors will be listed.

1, Introduction framework

1.1 infrastructure

Exception related
  • ServiceException
  • ... and other custom serviceexceptions
DefaultExceptionInterceptor

Where global exceptions are handled, we can specify which exceptions are intercepted and handled here.
Multilingual processing tools:

  • All classes under the resources package
  • Basic Toolkit: all classes under utils package
  • Error code: error
  • Log: L

1.2 customizing multiple languages

We can define multi language packages under the classpath, and the file name is used as the language name.
Here I introduce two languages:

  • en-US.ini: American English
  • zh_CN.ini: Chinese

Note that in files in different languages

  • As the same query code, the number and content of key s must be consistent
  • As information in different languages, value can be different, but cannot be empty

So far, we have built the basic framework

2, Customize specific ServiceException

Extending ErrorCode and ServiceException and customizing specific exception services and exception codes can facilitate our management of various exceptions.
This chapter customizes the exception service through the ArticleService service

2.1 ServiceException

    private static final long serialVersionUID = -3121925981104998575L;
    // Error code
    private int errorCode;
    // Error parameter set
    private Object[] errorParams;
    //Error data
    private Map<?, ?> errorData;
  • errorCode: error code, corresponding to the key of the multilingual file
  • errorParams: bad parameter. Extended information of exception
  • errorData: bad data. Extended information of exception

3, Unified exception handling

The DefaultExceptionInterceptor class is a specific place to catch and handle the exception specified by us. It is the cornerstone of our multilingual processing mechanism. Therefore, understanding this class is very important.
Before explaining this class, you need to understand the following annotation functions:

@ControllerAdvice: @ ControllerAdvice is a @ Component used to define @ ExceptionHandler, @ InitBinder and @ ModelAttribute methods, which is applicable to all using @ RequestMapping methods. As the name suggests, this is an enhanced Controller. Using this Controller, three functions can be realized:

  • Global exception handling
  • Global data binding
  • Global data preprocessing

We will not discuss global data binding and global data preprocessing for the time being.
In short, the annotation can capture all exceptions created by the @ RequestMapping method by defining @ ExceptionHandler, and the @ RequestMapping method is the place where we call the Service, so it can capture all business exceptions.

@ExceptionHandler: acts on the method. The attribute is an exception class object, which is used to intercept and handle the target exception.

Interception processing logic analysis

    @ExceptionHandler(Throwable.class) // Here we catch all exceptions
    public ModelAndView handleError(HttpServletRequest request, HandlerMethod handlerMethod, Throwable ex) {
        // 1. Handling of ServiceException and non ServiceException
        L.error(ex); // Print error source information in the log
        // If the exception is not a ServiceException, the current request information is printed
        if (!(ex instanceof ServiceException)) {
            ApiLog.log(request, null);
        }

        ServiceException se;
        // If it is not a ServiceException, continue printing the error message
        // Set the current exception to a ServiceException for an unknown error
        if (ex instanceof ServiceException) {
            se = (ServiceException) ex;
        } else {
            L.error(ex);
            se = new ServiceException(ErrorCode.ERR_UNKNOWN_ERROR);
        }
        // Get error code
        int errorCode = se.getErrorCode();
        // 2. Language processing
        // Core: select the language, pass the error code and error parameters, and get the error Message
        String errorMsg = LocaleBundles.getWithArrayParams("sdwe", "err." + errorCode, se.getErrorParams());
        // 3. Wrap the error information into a Map set
        Map<String, Object> error = new HashMap<>();
        error.put("errcode", errorCode);
        error.put("errmsg", errorMsg);
        // And add the error data we passed into the Map collection
        if (se.getErrorData() != null) {
            error.put("errdata", se.getErrorData());
        }
        // Convert the error information into a Json string, add it into the of ModelAndView, and put it into the process of view processing.
        return new ModelAndView(new JsonView(error));
    }

We can see that the whole exception handling is divided into three parts:

  1. Log printing processing of ServiceException and non ServiceException
  2. language processing
  3. The wrapper error message is a Map collection

4, Multilingual processing mechanism

Through the analysis of exception handling, we can see that
LocaleBundles.getWithArrayParams(“sdwe”, “err.” + errorCode, se.getErrorParams()); It is a multi language mechanism that provides us with an interface for customized language information services. Therefore, we need to analyze this method and understand the logic of the whole mechanism through this method.
In this chapter, it is recommended to analyze while debugging.

4.1 LocaleBundleOptions and CompileOptions (configuration object)

LocaleBundleOptions is an internal class of simplelocal bundle (inheriting from LocaleBundle), which * * contains the relevant configuration information of simplelocal bundle. * * * when initializing simplelocal bundle, the relevant information of placeholder configuration will be automatically added to the built-in CompileOptions (placeholder configuration object).

  • strictMode: whether strict mode is enabled
  • defaultLocale: default language
  • prefLocales: list of optional languages
  • Escape specialchars: filter escape characters
  • compileStartToken: unknown
  • compileEndToken: unknown
  • logKey: unknown

4.2 simple locale bundle (implemented in multiple languages)

Simplelocal bundle is the owner and manager of multilingual data. By managing this class, localbundles provides us with an interface to use.

  • writeLock: object lock, byte[0], saving space
  • initialized: whether the initialization is completed
  • bundlesMap: language data warehouse
  • options: configuration information
  • compileOptions: compilation information
    Configuration information classes can be passed through construction methods.

4.3 LocaleBundles (LocaleBundle management class)

LocaleBundles is our multilingual management class, which internally maintains a simple localebundle public object that contains real multilingual data information.

@StaticInit: this annotation is a custom annotation, which is processed in StaticBootstrap. The reflection framework Reflections is used to scan all classes under the specified package after the Bean is constructed. Filter and obtain all class objects containing @ StaticInit annotation for some log printing. Don't pay attention to this thing.

Initialization of simplelocal bundle

static code block initializes BUNDLE (internally maintained simplelocal BUNDLE).

			// Initialize language list
			String[] locales = new String[]{"zh_CN","en_pea"};
            // Print language information to the log
            L.warn("Locales: " + StringUtil.join(locales, ","));

            // Configure simplelocal bundle
            LocaleBundleOptions options = new LocaleBundleOptions();
            options.setDefaultLocale(locales[1]);
            options.setPrefLocales(locales); // Configure a list of all language options
            options.setCompileStartToken("{"); // Set token start character
            options.setCompileEndToken("}"); // Set token end character
            BUNDLE = new SimpleLocaleBundle(options); // Put the configuration information into simplelocal bundle.

next

		  for (String local : locales) {
                local = local.trim(); // Remove optional language spaces
                java.util.Map<String, String> props = FileUtil.readProperties(R.getStream("local/" + local + ".ini"),
                        StringUtil.UTF8, false); // Load the current language file and put the data into the Map collection (read from the local/xxx.ini file under the classpath). Escape characters are not processed here
                for (Entry<String, String> entry : props.entrySet()) {
                    String key = StringUtils.trimToNull(entry.getKey()); // Remove spaces from the key. If it is empty, return null
                    String value = StringUtils.trimToNull(entry.getValue()); // Remove spaces from value and return null if it is empty
                    if (key == null || value == null) {
                        continue;
                    } // If there is a null value, the next entry iteration is performed
                    BUNDLE.put(key, local, value); // Otherwise, it will be put into the Language Query Library of the localized data BUNDLE. See the notes on this method for details
                }
            }

Let's focus on bundle Put method (filling the warehouse with data)

protected void put(String key, String locale, String value) throws Exception {
        if (StringUtil.isEmpty(key)) {
            throw new IllegalArgumentException("Bad key");
        } else if (StringUtil.isEmpty(locale)) {
            throw new IllegalArgumentException("Bad locale");
        } else {
            value = StringUtil.trimToNull(value);
            if (value == null) {
                throw new IllegalArgumentException("Bad value");
            } else {
            	// Whether character escape processing is enabled in configuration information
                if (this.options.escapeSpecialChars) {
                	// Character escape processing
                    value = StringUtil.escapeSpecialChars(value);
                }
				// Add object lock to prevent concurrent exceptions
                synchronized(this.writeLock) {
                    Map<String, String> table = (Map)this.bundlesMap.get(key);
                    if (table == null) {
                        table = new ConcurrentHashMap();
                        this.bundlesMap.put(key, table);
                    }

                    ((Map)table).put(locale, value);
                }
            }
        }
    }

Method summary:

effect:

  1. Take the key, equivalent to the code code, as the key in the language query library Map (ConcurrentHashMap set).
  2. Put locale (Language) and value (msg) into a table (ConcurrentHashMap set) as the value in the language query library Map

Details:

  1. The ConcurrentHashMap collection is used to store multilingual data because these resources are the shared resources of the identified static, and thread safety should be ensured.
  2. If localization is not required, there is no need to waste Map space. Therefore, the initialization size of the language query library is 0, and the object lock is byte[0]
  3. The put method throws an exception if a parameter is empty
  4. If the escape specialchars property of LocaleBundleOptions is true, the escape character processing will be performed on value (msg), which is true by default
  5. Final map When put, it will lock to prevent concurrent exceptions

Finally, call BUNDLE.. finishPut(); (check the validity of warehouse data)

protected void finishPut() {
        Set<String> locales = null;
        Iterator var2 = this.bundlesMap.entrySet().iterator();
		
        String key;
        Set theLocales;
        do {
            while(true) {
                Map table;
                do {
                    if (!var2.hasNext()) {
                        this.initialized = true;
                        return;
                    }

                    Entry<String, Map<String, String>> bundleEntry = (Entry)var2.next();
                    key = (String)bundleEntry.getKey();
                    table = (Map)bundleEntry.getValue();
                    if (table.get(this.options.getDefaultLocale()) == null) {
                        throw new RuntimeException(this.wrapLogMessage("No default value set for key: " + key, this.options));
                    }
                } while(!this.options.strictMode);

                if (locales != null) {
                    theLocales = table.keySet();
                    break;
                }

                locales = table.keySet();
            }
        } while(theLocales.size() == locales.size() && theLocales.containsAll(locales));

        throw new RuntimeException(this.wrapLogMessage("Missing some locales for key: " + key, this.options));
    }

Summary of the method:

  • De duplication of key
  • If the strict mode is not set for the optional language list (options), verify whether the key s and value s of all languages are consistent. If they are missing, an error will be reported
  • If all are successful, the ID is initialized successfully.
getWithArrayParams for LocaleBundles

This method is the interface for us to obtain Message in the specified language. It is very important to understand the usage and details of this interface.
Parameters:

  • locale: the selected language, corresponding to our language file name
  • key: error code
  • params: parameter list, corresponding to the parameter array (errorParams) we passed.

After entering this method, we can see that getRaw() returns the corresponding Message for us. Then enter this method

First, check whether the comfort operation is completed (in the finishPut method in the previous section, if the check data is qualified, the initialization operation will be marked as successful)

 if (!this.initialized) {
            throw new RuntimeException("Does not finish init");
        }

Then go to the data warehouse to find the language information of the parameter. If the parameter has no corresponding language Message information, null is returned, which means that we do not display data at the front end.

Map<String, String> table = (Map)this.bundlesMap.get(key);
            if (table == null) {
                return null;
            } 

If the language information corresponding to the key exists, check whether there is a message in the language we need.
Here, first judge whether the locale we passed in is empty. If it is not empty

else {
      if (locale != null) {
          locale = LocaleUtil.findSupportLocale(locale, table.keySet());
          if (locale != null) {
          return (String)table.get(locale);
          }
      }

LocaleUtil.findSupportLocale(locale, table.keySet()); Language support processing:
If the language is included in the data warehouse, the language will be returned directly
If the language is not included in the data warehouse, but if the following conditions are met:

  1. This language is equal to the parent language of a language in the data warehouse (the content on the left of the first underline), and the language in the warehouse is returned
  2. The parent language of this language is equal to a language of the data warehouse, and the language in the warehouse is returned
  3. The parent language of this language is equal to a parent language of the data warehouse, and the language in the warehouse is returned

If there is support for this language, the message message of this language will be returned directly

Continue, if the locale we passed in is null or there is no support for the language we passed in.
Remember some pre selected language lists stored when we initialize the configuration? this.options.getPrefLocales(); That's it. We get the list here, iterate over the list, and return the first message value that can find the corresponding language in the warehouse.

				String[] preferencedLocales = this.options.getPrefLocales();
                if (preferencedLocales != null) {
                    String[] var5 = preferencedLocales;
                    int var6 = preferencedLocales.length;

                    for(int var7 = 0; var7 < var6; ++var7) {
                        String prefLocale = var5[var7];
                        String value = (String)table.get(prefLocale);
                        if (value != null) {
                            return value;
                        }
                    }
                }

                return (String)table.get(this.options.getDefaultLocale());

If you can't find the corresponding language information, select message in the default language we set earlier.

return (String)table.get(this.options.getDefaultLocale());

After analyzing the complete getRow, let's sort out our ideas:

  1. First, determine whether initialization is complete
  2. Try to find the language message from the data warehouse with the language we passed
  3. Check whether the language we pass is supported. If it is not supported or we do not pass the language, continue. Otherwise, the supported language message will be returned.
  4. Try to find the language from our pre selected language list. If the language message can be found, return
  5. If it cannot be found, the default language set by us will be used to find the language from the data warehouse, and the message will be returned

So far, we have completed the acquisition of language message

Then, judge whether we have passed in a parameter list. If not, return it directly. If it exists, replace the placeholder. How to deal with it?

            Map<String, Object> context = new HashMap(params.length);

            for(int i = 0; i < params.length; ++i) {
                Object param = params[i];
                context.put(String.valueOf(i), param);
            }

            return PlaceholderUtil.compile(text, context, this.compileOptions, new LocaleBundle.LocaleCompileHandler(locale));
        }

First, take the value in the parameter list as value, 0-params Lenght is put into the Map collection as a key.
Then call placeholderutil Compile method to replace placeholders
Remember the startToken and endToken parameters we passed in when initializing the configuration? This is to set the package symbol of our placeholder. If we do not initialize, our placeholder needs to be wrapped by ${} by default.
This process is to splice the parameters corresponding to {0}, {1}, {2}... With message. Of course, we can call a placeholder multiple times.
Test:



So far, we have completed the analysis of the entire getWithArrayParams () method

5, Usage Summary

5.1 adding multiple languages

The first step is to add language files, such as English language en ini

  • The key s of files in different languages must be the same and the number must be the same
  • The value of all language files cannot be empty
  • The value of the language file can be written as a placeholder. What does the placeholder use to wrap its own configuration

Step 2: modify the initialization information of the warehouse

5.2 configuring warehouse information


here

  • strictMode: whether strict mode is enabled
  • defaultLocale: the default language
  • prefLocales: list of preselected languages
  • Escape specialchars: whether to escape the escape character. The default value is true
  • compileStartToken: the start character of the compilation detection placeholder. The default is${
  • compileEndToken: the end character of the compilation detection placeholder. The default is}

5.3 getWithArrayParams() parameter interpretation

locale: the selected language

If it is null, select the first language in the preselected language list that exists in the warehouse. If none of the preselected languages in the list exists in the warehouse, select the default language.

If it is not null, check whether the language is supported:

  1. This language is equal to the parent language of a language in the data warehouse (the content on the left of the first underline), and the language in the warehouse is returned
  2. The parent language of this language is equal to a language of the data warehouse, and the language in the warehouse is returned
  3. The parent language of this language is equal to a parent language of the data warehouse, and the language in the warehouse is returned
    If they are not satisfied, they are treated as null.
key: in the corresponding language file Last number
params: parameter list

Take the index in the parameter list as a placeholder for the value we fill in the file. The placeholder is wrapped by the compileStartToken and compileEndToken defined by us, corresponding to the data in the parameter list. Refer to the test at the end of the previous chapter for specific usage.

Keywords: Java Spring Boot

Added by OM2 on Sun, 26 Dec 2021 17:12:56 +0200