1. Introduction


There is an old Chinese saying, “If one commits the same mistake once, twice or three times, he can be forgiven, but if he commits the same mistake more than three times, he cannot be forgiven. Some people point out that this “three” is an imaginary number, used to refer to multiple times, so “nothing but three” does not include “three”. As for the “nothing but three” package does not include “three”, it may have something to do with everyone’s bottom line, which belongs to the scope of philosophy and is not within the scope of this article.



Writing code is the same, the same code “pit”, step on the first time called “long experience”, step on the second time called “deepen impression”, step on the third time called “not long mind”, step on more than three times called “hopeless”. In this paper, the author summarizes some code pit, describes the phenomenon of the problem, analyzes the problem, and gives the method to avoid the pit. I hope you can avoid this kind of code pit in advance when you encounter it in your daily coding.


1. Object comparison method



The objects.equals method in JDK1.7 makes it easy to compare Objects and avoid tedious null-pointer checking.


1.1. Problem symptoms

Before JDK1.7, when determining whether a short, int, or long wrapper data type is equal to a constant, we used to write:
Short shortValue = (short)12345; System.out.println(shortValue == 12345); //trueSystem.out.println(12345 == shortValue); // trueInteger intValue = 12345; System.out.println(intValue == 12345); //trueSystem.out.println(12345 == intValue); // trueLong longValue = 12345L; System.out.println(longValue == 12345); //trueSystem.out.println(12345 == longValue); // trueCopy the code


Since JDK1.7, the objects.equals method has been provided and functional programming is recommended. The code changes as follows:


Short shortValue = (short)12345; System.out.println(Objects.equals(shortValue, 12345)); //falseSystem.out.println(Objects.equals(12345, shortValue)); // falseInteger intValue = 12345; System.out.println(Objects.equals(intValue, 12345)); //trueSystem.out.println(Objects.equals(12345, intValue)); // trueLong longValue = 12345L; System.out.println(Objects.equals(longValue, 12345)); //falseSystem.out.println(Objects.equals(12345, longValue)); // falseCopy the code


Why would replacing == with objects.equals result in a different output?




1.2. Problem analysis



By decomcompiling the first code, we get the statement “System.out.println(shortValue == 12345);” The bytecode instructions of
7   getstatic java.lang.System.out : java.io.PrintStream [22]10  aload_1 [shortValue]11  invokevirtual java.lang.Short.shortValue() : short [28]14  sipush 1234517  if_icmpne 2420  iconst_121  goto 2524  iconst_025  invokevirtual java.io.PrintStream.println(boolean) : void [32]Copy the code


It turns out that the compiler determines the basic data type corresponding to the wrapper data type and compares the instructions of this basic data type (sipush and IF_ICmpne in the bytecode instructions above, etc.), which is equivalent to the compiler automatically forcing the data type of the constant.




Why doesn’t the compiler automatically cast constants to their data types after objects.equals? By decomcompiling the second code, we get the statement “System.out.println(objects.equals (shortValue, 12345));” The bytecode instructions of
7   getstatic java.lang.System.out : java.io.PrintStream [22]10  aload_1 [shortValue]11  sipush 1234514  invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [28]17  invokestatic java.util.Objects.equals(java.lang.Object, java.lang.Object) : boolean [33]20  invokevirtual java.io.PrintStream.println(boolean) : void [39]Copy the code


It turns out that the compiler literally thinks the constant 12345 defaults to an int, so it automatically converts to the wrapper data type Integer.


In the Java language, the default data type for integers is int and the default data type for decimals is double.
Let’s examine the objects.equals method:
public static boolean equals(Object a, Object b) {   return(a == b) || (a ! = null && a.equals(b)); }Copy the code


The statement “a.equals(b)” will use the short.equals method.




The Short. Equals method is implemented as follows:
public boolean equals(Object obj) {   if (obj instanceof Short) {       return value == ((Short)obj).shortValue();  }   return false; }Copy the code


Println (objects.equals (shortValue, 12345)); Objects.equals (Short) and objects.equals (Integer) are of different types, so the final comparison result must be false. Similarly, the statement “System.out.println(objects.equals (intValue, 12345));” Since the two objects. equals parameters are of the same type, both of the wrapper data type Integer and have the same value, the final comparison result must be true.




1.3. Method of pit avoidance


1. Keep good coding habits and avoid automatic conversion of data types


To avoid automatic conversion of data types, it is more scientific to declare constants as corresponding base data types.


The first piece of code could look like this:
Short shortValue = (short)12345; System.out.println(shortValue == (short)12345); //trueSystem.out.println((short)12345 == shortValue); // trueInteger intValue = 12345; System.out.println(intValue == 12345); //trueSystem.out.println(12345 == intValue); // trueLong longValue = 12345L; System.out.println(longValue == 12345L); //trueSystem.out.println(12345L == longValue); // trueCopy the code


The second piece of code could be written like this:

Short shortValue = (short)12345; System.out.println(Objects.equals(shortValue, (short)12345)); //trueSystem.out.println(Objects.equals((short)12345, shortValue)); // trueInteger intValue = 12345; System.out.println(Objects.equals(intValue, 12345)); //trueSystem.out.println(Objects.equals(12345, intValue)); // trueLong longValue = 12345L; System.out.println(Objects.equals(longValue, 12345L)); //trueSystem.out.println(Objects.equals(12345L, longValue)); // trueCopy the code


2. Use development tools or plug-ins to catch data type mismatches early




In Eclipse’s Problems window, we see the following prompt:
Unlikely argument type for equals(): int seems to be unrelated to ShortUnlikely argument type for equals(): Short seems to be unrelated to intUnlikely argument type for equals(): int seems to be unrelated to LongUnlikely argument type for equals(): Long seems to be unrelated to intCopy the code


Scanning through the FindBugs plugin, we see this warning:


Call to Short.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Integer.equals(Short) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Long.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Call to Integer.equals(Long) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]Copy the code



3. Conduct routine unit tests and try to find problems in the development phase


Don’t make small changes so you don’t need to unit test. Bugs tend to show up in your overconfident code. Problems like this can be found with a single unit test.




2. Unpack ternary expressions


Ternary expressions are a fixed syntactic format in Java coding: “Conditional expressions? Expression 1: Expression 2 “. The logic of a ternary expression is: “If the conditional expression is true, expression 1 is executed, otherwise expression 2 is executed.”


2.1. Symptom

boolean condition = false; Double value1 = 1.0 D; Double value2 = 2.0 D; Double value3 = null; Double result = condition ? value1 * value2 : value3; // Throw a null pointer exceptionCopy the code


When condition = false, Double value3 is assigned to Double result. NullPointerException = false




2.2. Problem analysis


By decompiling the code, we get the statement “Double result = condition? value1 * value2 : value3;” The bytecode instructions of
17  iload_1 [condition]18  ifeq 3321  aload_2 [value1]22  invokevirtual java.lang.Double.doubleValue() : double [24]25  aload_3 [value2]26  invokevirtual java.lang.Double.doubleValue() : double [24]29  dmul30  goto 3833  aload 4 [value3]35  invokevirtual java.lang.Double.doubleValue() : double [24]38  invokestatic java.lang.Double.valueOf(double) : java.lang.Double [16]41  astore 5 [result]43  getstatic java.lang.System.out : java.io.PrintStream [28]46  aload 5 [result]Copy the code


At line 33, the Double object value3 is loaded into the operand stack; At line 35, the doubleValue method of the Double object value3 is called. At this point, since value3 is null, calling doubleValue must throw a null-pointer exception. But why convert the empty object value3 to the underlying data type Double?




Refer to relevant information and get the type conversion rules of ternary expressions:
  1. If the two expressions are of the same type, the return type is that type.
  2. If the two expressions are of different types but cannot be converted, the return type is Object.
  3. If two expressions have different types but can be converted, the wrapper data type is first converted to the basic data type, and then converted according to the basic data type conversion rules (Byte
According to the rule analysis, expression 1 (value1 * value2) returns the underlying data type double after calculation, and expression 2 (value3) returns the wrapper data type double. According to the type conversion rules of ternary expressions, the final return type is the underlying data type double. Therefore, when the condition is equal to false, the null object value3 needs to be converted to the underlying data type double. Value3’s doubleValue method is called to throw a null pointer exception.


The type conversion rule for ternary expressions can be verified with the following example:
boolean condition = false; Double value1 = 1.0 D; Double value2 = 2.0 D; Double value3 = null; Integer value4 = null; Return type Double result1 = condition? value1 : value3; Double result2 = condition? Double result2 = condition? value1 : value4; // Return type double, no null pointer exception double result3 =! condition ? value1 * value2 : value3; Double result4 = condition? value1 * value2 : value3;Copy the code

2.3. Method of pit avoidance


1. Avoid ternary expressions and use if-else statements instead


If a ternary expression has arithmetic calculations and wrapped data types, consider using if-else statements instead. Rewrite the code as follows:
boolean condition = false; Double value1 = 1.0 D; Double value2 = 2.0 D; Double value3 = null; Double result;if(condition) { result = value1 * value2; }else{ result = value3; }Copy the code


2. Try to use basic data types to avoid automatic conversion of data types




If a ternary expression has arithmetic calculations and wrapped data types, consider using if-else statements instead. Rewrite the code as follows:
boolean condition = false; Double value1 = 1.0 D; Double value2 = 2.0 D; Double value3 = 3.0 D; double result = condition ? value1 * value2 : value3;Copy the code


3. Conduct coverage unit tests and try to find problems in the development stage


Problems like this can be detected in advance by writing some unit test cases and doing some coverage testing.




3. Generic object assignment


Java generics, a new feature introduced in JDK1.5, are essentially parameterized types, that is, using a data type as a parameter.




3.1. Problem phenomenon


When doing user data paging query, the following code was written because of a clerical error:


1, PageDataVO. Java:
/** Pagination data VO class */@Getter@Setter@ToString@NoArgsConstructor @allargsConstructorPublic class PageDataVO<T> {/** Total number */ private Long totalCount; /** private List<T> dataList; }Copy the code


2, UserDAO. Java:
Public Long countUser(@param () public Long countUser(@param ())"query") UserQueryVO query); Public List<UserDO> queryUser(@param)"query") UserQueryVO query); }Copy the code


3, UserService. Java:


/** UserDAO */ @autowired private UserDAO UserDAO; Public PageDataVO<UserVO> queryUser(UserQueryVO query) {List<UserDO> dataList = null; Long totalCount = userDAO.countUser(query);if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {           dataList = userDAO.queryUser(query);      }       return new PageDataVO(totalCount, dataList);  }}Copy the code


4, UserController. Java:


/** User controller class */@Controller@RequestMapping("/user"Public class UserController {/** UserController */ @autowired private UserService UserService; @responseBody @requestMapping (value =)"/query". method = RequestMethod.POST) public Result<PageDataVO<UserVO>> queryUser(@RequestBody UserQueryVO query) { PageDataVO<UserVO> pageData = userService.queryUser(query);return ResultBuilder.success(pageData);  }}Copy the code


The above code doesn’t have any compilation problems, but it does return some of the confidential fields in UserDO to the front end. Careful readers may have noticed that in the queryUser method of the UserService class the statement “Return New PageDataVO(totalCount, dataList);” PageDataVO<UserVO>; PageDataVO<UserVO>; PageDataVO<UserVO>; PageDataVO<UserVO>;




The question is: why don’t development tools report compilation errors?


3.2. Problem analysis


For historical reasons, parameterized types and primitive types need to be compatible. Let’s take ArrayList as an example to see how this works.


Previously written:
ArrayList list = new ArrayList();Copy the code


Now written:


ArrayList<String> list = new ArrayList<String>();Copy the code


For compatibility with previous code, passing values between various object references must result in the following:


ArrayList list1 = new ArrayList<String>(); ArrayList<String> list2 = new ArrayList();Copy the code


As a result, the Java compiler is compatible with both types, and compilation errors do not occur, but compilation alarms do occur. However, my development tools compile without any alarms.




To analyze the problem we encountered, we actually hit both:
List

= List

;

PageDataVO

; PageDataVO

;



The net effect is that we magically assign the List

object to List

.



The root of the problem is that we did not enforce type checking when initializing the PageDataVO object.


3.3. Pit avoidance method



1. The diamond syntax is recommended when initializing generic objects


In the “Alibaba Java Development Manual”, there is such a recommended rule:
In JDK7 and above, use the diamond syntax or skip all generic definitions. Note: Diamond generics, known as diamond, use <> directly to refer to the previously specified type. Is:
// <> diamond mode HashMap<String, String> userCache = new HashMap<>(16); ArrayList<User> users = new ArrayList(10);Copy the code
In fact, it is not recommended to initialize generic objects. This avoids type checking, which causes the problem above.


When initializing generic objects, it is recommended to use the diamond syntax as follows:
return new PageDataVO<>(totalCount, dataList);Copy the code


Now, in Eclipse’s Problems window, we see an error like this:

Cannot infer type arguments for PageDataVO<>Copy the code


So we know we forgot to convert the List<UserDO> object to the List<UserVO> object.




2. When doing unit tests, you need to compare data content


When it comes to unit testing, running well is one metric, but getting the data right is more important.




4. Copy generic properties


Spring’s beanutils.copyProperties method is a useful property copy utility method.



4.1. Symptom



According to the database development specification, the database table must contain the ID, GMT_CREATE, gmT_MODIFIED three fields. Where, the id field, depending on the amount of data, may be int or long (note: Ali specification must be long, int or long is allowed for illustration purposes).


So, by pulling these three fields out, we define a BaseDO base class:
/** BaseDO */@Getter@Setter @toStringPublic BaseDO<T> {private T id; private Date gmtCreate; private Date gmtModified; }Copy the code


For the user table, we define a UserDO class:


/** @Getter@Setter @toStringPublic extends BaseDO<Long>{private String name; private String description; }Copy the code


For the query interface, a UserVO class is defined:


@Getter@Setter @toStringPublic static class UserVO {private Long id; private String name; private String description; }Copy the code


To realize the query user service interface, the implementation code is as follows:


/** UserDAO */ @autowired private UserDAO UserDAO; Public List<UserVO> queryUser(UserQueryVO query) {// List<UserDO> userDOList = userDAO.queryUser(query);if (CollectionUtils.isEmpty()) {           returnCollections.emptyList(); } List<UserVO> userVOList = new ArrayList<>(userdolist.size ());for(UserDO userDO : userDOList) { UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); } // Return to the user listreturn userVOList;  }}Copy the code


Through testing, we found a problem — the value of the user ID was not returned by calling the query user service interface.


[{"description":"This is a tester."."name":"tester"},... ]Copy the code


4.2. Problem analysis



UserVO and UserDO id fields are of type Long and cannot be converted. Try manual assignment as follows:
for(UserDO userDO : userDOList) { UserVO userVO = new UserVO(); userVO.setId(userDO.getId()); userVO.setName(userDO.getName()); userVO.setDescription(userDO.getDescription()); userVOList.add(userVO); }Copy the code


After testing, the above code returns normal results, and the value of the user ID returns successfully.




The problem, then, is with the beanutils.copyProperties tool method. Run in Debug mode, go inside the beanutils.copyProperties tool method and get the following data:


The return type of UserDO’s getId method is not Long, but Object. The classutils. isAssignable utility method determines whether an Object can be assigned to a Long, and of course returns false so that the property cannot be copied.


Why doesn’t the author consider “get the value of the attribute first and decide whether to assign it later”? The suggested code is as follows:
Object value = readMethod.invoke(source);if(Objects.nonNull(value) && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) { ... // assign code}Copy the code


4.3. Pit avoidance method



1. Don’t blindly trust third party toolkits, any toolkit can have problems


In Java, there are many third-party toolkits, such as Apache’s Commons-Lang3, Commons-Collections, Google’s Guava… These are all good third party toolkits. However, don’t blindly trust third-party toolkits; any toolkit can have problems.


2. If you need to copy fewer attributes, you can copy them manually


The main advantage of using beanutils.copyProperties is that it saves code, but the main disadvantage is that it degrades program performance. Therefore, if fewer attributes need to be copied, you can manually encode the attributes for copying.


3. Always do unit tests and always compare data content


After writing code, be sure to unit test and compare data content. Don’t assume that the toolkit is mature, the code is simple, and problems are unlikely.




5. The Set object is weighted


In the Java language, a Set data structure can be used for object reweighting. Common Set classes include HashSet, LinkedHashSet, and so on.


5.1. Symptoms


Write a city helper class to read city data from a CSV file:
Public static void main(String[] args) {Collection<City> cityCollection =readCities2("cities.csv"); log.info(JSON.toJSONString(cityCollection)); } public static Collection<City>readCities(String fileName) {       try (FileInputStream stream = new FileInputStream(fileName);           InputStreamReader reader = new InputStreamReader(stream, "GBK");           CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.withHeader())) {           Set<City> citySet = new HashSet<>(1024);           Iterator<CSVRecord> iterator = parser.iterator();           while (iterator.hasNext()) {               citySet.add(parseCity(iterator.next()));          }           return citySet;      } catch (IOException e) {           log.warn("Read all city exceptions", e);      }       returnCollections.emptyList(); } private static City parseCity(CSVRecord record) {City City = new City(); city.setCode(record.get(0)); city.setName(record.get(1));returncity; } @getter@setter@toString private static class City {/** private static class City code */ private String code; /** city name */ private String name; }}Copy the code


The HashSet data structure is used in the code to avoid the duplication of city data and to enforce the reweighting of the read city data.




If the file content is as follows:
Code, name 010, Beijing 020, Guangzhou 010, BeijingCopy the code


The parsed JSON result is as follows:


[{"code":"010"."name":"Beijing"}, {"code":"020"."name":"Guangzhou"}, {"code":"010"."name":"Beijing"}]Copy the code


However, the city of “Beijing” was not ranked.


5.2. Problem analysis


When adding an object to a Set, the collection first computes the hashCode of the object to be added, using that value to get a location for the current object. If no object exists at that location, the Set assumes that the object does not exist in the Set and adds it to the Set. If an object exists at that location, then the equals method compares the object being added to the collection with the object at that location. If the equals method returns false, the collection considers that the object does not exist in the collection and places the object after it. If equals returns true, the object is considered to already exist in the collection and will not be added to it. Therefore, the hashCode and equals methods are used to determine whether two elements are duplicated in the hash table. The hashCode method determines where data is stored in the table, and the Equals method determines whether the same data exists in the table.


The hashCode and equals methods of the Object class are used instead of overriding the hashCode and equals methods of the City class. Its implementation is as follows:

public native int hashCode(); public boolean equals(Object obj) {return(this == obj); }Copy the code


The hashCode method of the Object class is a local method that returns the address of the Object. The equals method of the Object class only compares objects for equality. Therefore, for the two identical Beijing data, because different City objects were initialized during parsing, the values of hashCode and Equals methods were different, which must be considered as different objects by Set. Therefore, no weighting was carried out.




We rewrite the hashCode and equals methods of the City class as follows:
@Getter@Setter @tostringPrivate static class City {/** private String code */ private String code; /** city name */ private String name; */ @override public Boolean equals(Object obj) {if (obj == this) {           return true;      }       if (Objects.isNull(obj)) {           return false;      }       if(obj.getClass() ! = this.getClass()) {return false;      }       returnObjects.equals(this.code, ((City)obj).code); } /** Override public inthashCode() {       return Objects.hashCode(this.code);  }}Copy the code


Support the test program again, and the JSON result after parsing is as follows:


[{"code":"010"."name":"Beijing"}, {"code":"020"."name":"Guangzhou"}]Copy the code


The result is correct and the city “Beijing” has been ranked.




5.3. Method of pit avoidance


1. A List can be used instead of a Set when the data is unique


When it is determined that the parsed city data is unique, there is no need to perform the rearrangement operation and the List can be used to store the data directly.
List<City> citySet = new ArrayList<>(1024); Iterator<CSVRecord> iterator = parser.iterator();while(iterator.hasNext()) { citySet.add(parseCity(iterator.next())); }return citySet;Copy the code


2. Map can be used instead of Set when certain data is not unique


If the resolved city data is not unique, you need to install the city name to perform reassignment. You can use Map to store the data. Why not implement the hashCode method of the City class, and then use HashSet to implement weights? First, you don’t want to put business logic in model DO classes; Secondly, put the weight field in the code, easy to read, understand and maintain the code.
Map<String, City> cityMap = new HashMap<>(1024); Iterator<CSVRecord> iterator = parser.iterator();while(iterator.hasNext()) { City city = parseCity(iterator.next()); cityMap.put(city.getCode(), city); }return cityMap.values();Copy the code


3. Follow the Java language specification and override the hashCode and equals methods
Custom classes that do not override the hashCode and Equals methods should not be used in sets.




6. Public method proxy


The SpringCGLIB agent generates a proxy class that inherits the proxyed class and implements the proxy by overriding non-final methods in the proxyed class. Therefore, the SpringCGLIB proxy class cannot be final, nor can the proxy method be final, due to the inheritance mechanism.


6.1. Problem symptoms



As a simple example, only the superuser has permission to delete the company, and all service functions are intercepted by AOP to handle exceptions. Example code is as follows:


1, UserService. Java:
/** User service */ @servicePublic class UserService {/** superUser */ private User superUser; /** Set superuser */ public voidsetSuperUser(User superUser) { this.superUser = superUser; } /** Get super User */ public final UsergetSuperUser() {       return this.superUser;  }}Copy the code


2, CompanyService. Java:


/** Private CompanyDAO CompanyDAO; /** Private CompanyDAO CompanyDAO; /** @autowired private UserService UserService; Public void deleteCompany(Long companyId, Long operatorId) {userservice.setsuperuser (new User(0L, new User);"admin"."Power user")); // Verify the superuserif(! Objects.equals(operatorId, userService.getSuperUser().getId())) { throw new ExampleException("Only superusers can delete companies."); } // Delete companydao.delete (companyId, operatorId); }}Copy the code


3, AopConfiguration. Java:


/** AOP configuration class */@Slf4j@Aspect @configurationPublic class AopConfiguration {/**"execution(* org.changyi.springboot.service.. *. * (..) )")   public Object around(ProceedingJoinPoint joinPoint) {       try {           log.info("Start calling service methods...");           returnjoinPoint.proceed(); } catch (Throwable e) { log.error(e.getMessage(), e); throw new ExampleException(e.getMessage(), e); }}}Copy the code


When CompanyService deleteCompany is called, a NullPointerException is thrown. The superuser obtained by calling the getSuperUser method of the UserService class is NULL. However, each time we force a superuser in the CompanyService deleteCompany method through the setSuperUser method of the UserService class, The superuser obtained from the getSuperUser method of the UserService class should not be null. In fact, this problem is caused by AOP proxies.




6.2. Problem analysis


When using the SpringCGLIB proxy class, Spring creates a class named UserService? EnhancerBySpringCGLIB????????? Proxy class of. Decompiling the proxy class yields the following main code:
public class UserService$$EnhancerBySpringCGLIB$$a2c3b345 extends UserService implements SpringProxy, Advised, Factory {  ......   public final void setSuperUser(User var1) {       MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;       if (var10000 == null) {           CGLIB$BIND_CALLBACKS(this);           var10000 = this.CGLIB$CALLBACK_0;      }       if(var10000 ! = null) { var10000.intercept(this, CGLIB$setSuperUser$0$Method, new Object[]{var1}, CGLIB$setSuperUser$0$Proxy);      } else{ super.setSuperUser(var1); }}... }Copy the code


As you can see, this proxy class inherits the UserService class and proxies the setSuperUser method, but not the getSuperUser method. So, when we call the setSuperUser method, we set the superUser field value of the original object instance; When we call the getSuperUser method, we get the superUser field value of the proxy object instance. If the final modifier of the two methods is interchanged, there is also the problem of getting null for the superuser.




6.3. Method of pit avoidance


1. Follow the CGLIB proxy specification strictly and do not add final modifiers to propped classes and methods


In strict accordance with the CGLIB proxy specification, proxyed classes and methods should not have final modifiers to avoid dynamic proxying that operates on different object instances (the original object instance and the proxy object instance), resulting in data inconsistency or null pointer problems.


2. Narrow down the scope of CGLIB proxy classes


By narrowing the scope of CGLIB proxy classes, we can save memory and improve the efficiency of function calls.




7. Public field proxy


– there was a pit in fastjson forcing an upgrade to 1.2.60.
public class ParseConfig { public final SymbolTable symbolTable = new SymbolTable(4096); . }Copy the code


Inheriting this class in our project, while being dynamically proxied by AOP, one line of code caused a bloodbath.






7.1. Symptom


We still use the example from the previous chapter, but remove the get and set methods and define a public field. Example code is as follows:


1, UserService. Java:
Public final User superUser = new User(0L,"admin"."Power user"); . }Copy the code


2, CompanyService. Java:


/** Private CompanyDAO CompanyDAO; /** Private CompanyDAO CompanyDAO; /** @autowired private UserService UserService; /** deleteCompany */ public void deleteCompany(Long companyId, Long operatorId) {// verify the superuserif(! Objects.equals(operatorId, userService.superUser.getId())) { throw new ExampleException("Only superusers can delete companies."); } // Delete companydao.delete (companyId, operatorId); }}Copy the code


3, AopConfiguration. Java:




Same as in the previous chapter aopConfiguration.java.


A NullPointerException was thrown when CompanyService deleteCompany was called. After debugging, the superUser variable of UserService is null. If you remove the AopConfiguration, there is no null pointer exception, indicating that the problem is caused by an AOP proxy.


7.2. Problem analysis



When using the SpringCGLIB proxy class, Spring creates a class named UserService? EnhancerBySpringCGLIB????????? Proxy class of. This proxy class inherits the UserService class and overrides all non-final public methods in the UserService class. However, this proxy class does not call the methods of the super base class; Instead, it creates a member userService and points to the original userService class object instance. There are now two object instances in memory: the original UserService object instance and a proxy object instance pointing to UserService. This proxy class is just a virtual proxy that inherits the UserService class and has the same fields as UserService, but it never initializes and uses them. Therefore, a default value of NULL is returned whenever a public member variable is retrieved from this proxy object instance.


7.3. Method of pit avoidance



1. When a field is determined to be immutable, it can be defined as a public static constant


When a field is determined to be immutable, it can be defined as a public static constant and accessed with the class name + field name. Class name + field name accesses public static constants, independent of the dynamic proxy for the class instance.
Public static final User SUPER_USER = new User(0L, 0L, 0L)"admin"."Power user");  ......}/** 使用代码 */if(! Objects.equals(operatorId, UserService.SUPER_USER.getId())) { throw new ExampleException("Only superusers can delete companies."); }Copy the code


2. When a field is determined to be immutable, it can be defined as a private member variable




When a field is determined to be immutable, it can be defined as a private member variable, providing a public method to get the value of the variable. When an instance of the class is dynamically proxied, the proxy method calls the proxied method, which returns the value of a member variable of the proxied class.
/** private User superUser = new User(0L,"admin"."Power user"); /** Get the superuser */ public UsergetSuperUser() {       returnthis.superUser; }... }/** use code */if(! Objects.equals(operatorId, userService.getSuperUser().getId())) { throw new ExampleException("Only superusers can delete companies."); }Copy the code


3, follow the JavaBean coding specification, do not define public member variables




Follow the JavaBean coding specification and do not define public member variables. The JavaBean specification is as follows:
(1) The JavaBean class must be a public class and set its access property to public, for example: public class User{…… }(2) A JavaBean class must have an empty constructor: the class must have a public constructor that takes no arguments. (3) A JavaBean class should have no public instance variables. (4) Properties should be accessed through a set of getter/setter methods.




Afterword.


Humans benefit from “analogy” thinking, which is human wisdom. When encountering new things, people often use similar known things as reference, which can accelerate the cognition of new things. And human beings are subject to “set” thinking, because the known things do not represent the new things, and people are easy to form preconceived concepts, resulting in the final misjudgment of the new things.