My official account:
MarkerHub, the website:
https://markerhub.com

For more selected articles, please click: Java notebook.md

Small Hub Guide:

The author takes adding, deleting and changing the shipping address as an example, and explains in detail how to design a good exception handling, including the use of Preconditions in Guava, Hibernate’s Hibernate -Validator, as well as how to handle exceptions and the logic of exception handling. The article is a bit long, but I still get a lot after reading it.


  • Source: lrwinx
  • https://lrwinx.github.io/

Introduction:

Exception handling is one of the essential operations in the program development, but how to handle the exception correctly and elegantly is really a knowledge, the author will talk about how I handle the exception according to my own development experience.

Since this article is an empirical one and does not cover the basics, if you have a vague idea of exceptions, check out the basics first.

How do I select an exception type

Type of exception

As we all know, The superclass for exceptions in Java is java.lang.Throwable, which has two more important subclasses, Java.lang. Exception(Exception) and java.lang.Error(Error), Error is managed by the JVM virtual machine, as we are familiar with OutOfMemoryError exceptions, so we will not focus on Error exceptions in this article, so we will detail the Exception exceptions.

Exception has a more important subclass called RuntimeException. RuntimeException or any other subclass that inherits from RuntimeException is called an unchecked Exception. Other subclasses that inherit from Exception are called Checked Exceptions. This article focuses on the two types of anomalies, checked and unchecked.

How to select an exception

From the author’s development experience, if in an application, you need to develop a method (such as a function of the service method), the method may appear if the exception, so you need to consider whether the anomaly appeared after the caller can handle, and if you want to call for processing, if the caller can handle, Throw a checked exception that you also want the caller to handle, reminding the caller that when using your method, he or she should consider handling if an exception is thrown.

Similarly, if you’re writing a method and you think it’s an accidental exception, theoretically, that you might encounter a problem at runtime that might not necessarily occur, and does not require the caller to show that the exception is used to determine the operation of the business process, An unchecked exception such as a RuntimeException can then be used.

Well, you may have read this passage many times and still find it hard to understand.

So, please follow my train of thought, slowly understand.

When do I throw an exception

The first question we need to know is, when do you throw an exception? The design of exceptions is convenient for developers to use, but it is not a misuse. I have asked a lot of friends when to throw exceptions, but few of them can give an accurate answer. The problem is simple. If you feel that some “problem” is unsolvable, you can throw an exception.

For example, if you’re writing a service and you see a problem somewhere along the line of code, throw an exception. Trust me, this is the best time to throw an exception.

What exception should I throw

Now that we know when we need to throw an exception, let’s ask ourselves what kind of exception we should actually use when we do throw an exception. Is it a checked exception or an unchecked exception?

To illustrate the problem, let’s start with a checked exception. For example, if a business logic needs to read some data from a file, the read operation may be due to a file being deleted or some other problem that makes it unfettable and causes a read error. To retrieve this data from a Redis or MySQL database, refer to the following code. GetKey (Integer) is the entry procedure.

public String getKey(Integer key){ String value; try { InputStream inputStream = getFiles("/file/nofile"); // Next, read the key value from the stream. Value =... ; } catch (Exception E) {return value =... ; } } public InputStream getFiles(String path) throws Exception { File file = new File(path); InputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(file)); } catch (FileNotFoundException e) {throw new Exception("I/O read error ", EquetCause ()); } return inputStream; }

OK, after looking at the above code, you may have some idea that checked exceptions can control obligation logic, yes, checked exceptions can control business logic, but remember not to use this, we should be reasonable to throw exceptions, because the program itself is the process. Abnormal function is only when you do not find an excuse, it does not as a control program flow entry or exit, if such use, is a function of the abnormal enlargement, this will lead to increased code complexity, coupling increases, reduce problems such as code readability.

Are such exceptions definitely not to be used? In fact, it is not, when there is a real need for such a time, we can use it, just remember, it is not really as a tool or means to control the process. So when exactly do you throw such an exception? Consider that if the caller makes an error, it must be handled by the caller. Only when such a requirement is met can we consider using checked exceptions.

Next, let’s look at RuntimeExceptions, which we actually see a lot, Such as Java. Lang. NullPointerException/Java. Lang. IllegalArgumentException, etc., so this exception when we throw?

When we are writing a method, we may accidentally encounter an error that we think might have occurred at runtime, and in theory, it does not force the caller to catch the exception when the program would execute normally without this problem.

For example, when passing a path, return a File object corresponding to the path:

public void test() { myTest.getFiles(""); {if} public File getFiles (String path) (null = = path | | "" equals (path)) {throw new NullPointerException (" path cannot be empty!" ); } File file = new File(path); return file; }

The above example shows that if the path is empty when the caller calls getFiles(String), then the null pointer exception (which is a subclass of RuntimeException) is thrown, and the caller does not have to show the try… The catch… Operation to enforce processing. This requires the caller to validate before calling such a method to avoid a RuntimeException. As follows:

public void test() { String path = "/a/b.png"; if(null ! = path && !" ".equals(path)){ myTest.getFiles(""); {}} public File getFiles (String path) if (null = = path | | "" equals (path)) {throw new NullPointerException (" path cannot be empty!" ); } File file = new File(path); return file; }

Which exception should you choose

To conclude, the difference between a RuntimeException and a checked exception is: Whether it is mandatory for the caller to handle this exception, if it is mandatory for the caller to handle it, then use the checked exception, otherwise select RuntimeException. In general, we recommend using a RuntimeException exception if you don’t have a specific requirement.

Scene introduction and technology selection

Architectural description

As we all know, traditional projects are developed based on the MVC framework, and this article focuses on the elegance of exception handling from a design that uses a RESTful style interface.

Focusing on the RESTful API layer (similar to the Controller layer on the Web) and the Service layer, we’ll look at how exceptions are thrown in the Service, and then how the API layer catches and transforms them.

The technologies used are: Spring-Boot, JPA (Hibernate), MySQL. If you are not familiar with these technologies, you will need to read the relevant materials.

Business scenario description

Select a relatively simple business scenario, taking the delivery address management in e-commerce as an example. When users purchase goods on the mobile terminal, they need to carry out the delivery address management. In the project, provide some API interfaces for mobile terminal to access, such as: Add receiving address, delete receiving address, change receiving address, default receiving address setting, receiving address list query, single receiving address query and other interfaces.

Construction Constraints

OK, this is a very basic business scenario set up, of course, no matter what the API operation, there are some rules:

Add shipping address: Enter parameter:

  • The user id
  • Receiving address entity information

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The required field for the shipping address cannot be blank
  • If the user does not already have a shipping address, set the default shipping address when this shipping address is created —

Delete shipping address: Enter parameter:

  • The user id
  • Receipt address ID

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The shipping address cannot be empty, and it does exist
  • Determine whether the shipping address is the user’s shipping address
  • Determine whether this shipping address is the default shipping address. If it is the default shipping address, it cannot be deleted

Change the receiving address: Enter parameter:

  • The user id
  • Receipt address ID

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The shipping address cannot be empty, and it does exist
  • Determine whether the shipping address is the user’s shipping address

Default address setting: Enter parameters:

  • The user id
  • Receipt address ID

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The shipping address cannot be empty, and it does exist
  • Determine whether the shipping address is the user’s shipping address

Receipt address list query: input parameter:

  • The user id

Constraints:

  • The user ID cannot be empty, and the user does exist

Single receiving address query: input parameter:

  • The user id
  • Receipt address ID

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The shipping address cannot be empty, and it does exist
  • Determine whether the shipping address is the user’s shipping address

Constraint judgment and technology selection

For the constraint conditions and function list listed above, I selected several typical exception handling scenarios for analysis: adding the receiving address, deleting the receiving address, and getting the receiving address list.

So what necessary knowledge should we have? Let’s take a look at the function of receiving address:

To add the receiving address, the user ID and the entity information of the receiving address are required to be verified. So for the non-empty judgment, how do we select the tool? The conventional judgment is as follows:

/** * public address address (Integer address address, address address){if(NULL! = uid){// handle... } return null; }

In the example above, it would be fine if only the uid was null, but it would be disastrous if some of the required attributes in address were null in the case of a large number of fields.

So how should we make these input judgments? Let me introduce two knowledge points:

  • The PreConditions class in Guava implements the determination of many input methods
  • Validation specification for JSR 303 (Hibernate -Validator for Hibernate implementation)

If you use both of these recommendation techniques, the input judgment becomes much easier. It is recommended to use these mature technologies and JAR toolkits, which can reduce a lot of unnecessary work. We just need to focus on the business logic. It’s not going to take any more time because of these input arguments.

How to elegantly design JAV exceptions

Domain is introduced

Depending on the project scenario, you need two domain models, one for the user entity and one for the address entity.

Address domain:

@Entity @Data public class Address { @Id @GeneratedValue private Integer id; private String province; // save private String City; Private String county; // private String county; // private Boolean isDefault; @ManyToOne(Cascade ={CascadeType.all}) @JoinColumn() private User User; }

User domains are as follows:

@Entity @Data public class User { @Id @GeneratedValue private Integer id; private String name; @onetomany (cascade= cascadetype. ALL,mappedBy="user",fetch = fetchtype. LAZY) private Set<Address> addresses; }

OK, the above is a model relationship, the user – shipping address relationship is 1-n relationship. The @data above uses a tool called Lombok, which automatically generates setters and getters, etc. It is very convenient for the interested reader to check it out.

The dao is introduced

Data connection layer, we use the framework of Spring-Data-JPA, which requires that we only need to inherit the interface provided by the framework, and name the methods according to the convention, then we can complete the database operation we want.

The user database operations are as follows:

@Repository
public interface IUserDao extends JpaRepository<User,Integer> {

}

Receiving address operation is as follows:

@Repository
public interface IAddressDao extends JpaRepository<Address,Integer> {

}

As you can see, our DAO only needs to extend JPRepository, which already does basic CURD and other operations for us. For more information on this project for Spring-Data, please refer to the Spring documentation. It does not compare with our study of anomalies.

Service Exception Design

OK, finally come to our point, we need to complete some parts of the service: add the shipping address, delete the shipping address, get the shipping address list.

First look at my Service interface definition:

Public interface IAddressService {/** * createAddress * @Param uid * @Param address * @Return */ address createAddress(Integer  uid,Address address); Void deleteAddress(Integer UID,Integer Aid); void deleteAddress(UID,Integer Aid); /** * check the Address of the user * @param uid * @return */ List<Address> listAddresses(Integer uid); }

Let’s focus on the implementation:

Add shipping address

First of all, let’s look at the constraints we sorted out earlier:

The arguments:

  • The user id
  • Receiving address entity information

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The required field for the shipping address cannot be blank
  • If the user does not already have a shipping address, set it to the default shipping address when it is created

Let’s start with the following code implementation:

@ Override public Address createAddress (Integer uid, Address the Address) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. User id cannot be empty, and the user exists Preconditions. CheckNotNull (uid); User user = userDao.findOne(uid); If (null == user){throw new RuntimeException(" User is not found!"); ); } / / 2. The shipping address of the necessary fields can't be empty BeanValidators. ValidateWithException (validator, address); If (objectutils.isEmpty (user.getaddresses ())){address.setisDefault(true); if(address.getaddresses ()) {address.setisDefault(true); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = address. SetUser (user); Address result = addressDao.save(address); return result; }

Where, the three constraints described above have been completed. Normal business logic can proceed only when all three constraints are met, otherwise an exception will be thrown (it is generally recommended here to throw a runtimeException).

Introduce the following techniques I used above:

1, Preconfitions. CheckNotNull (T T) this is the use of Guava the com.google.com mon. Base. The Preconditions for judgment, because of the large used in the validation of the service, So I recommend changing precontions to static import:

import static com.google.common.base.Preconditions.checkNotNull; 

Of course, the instructions on Guava’s GitHub suggest that we use it this way.

2, BeanValidators. ValidateWithException (validator, address); This is done using the Hibernate implementation of the JSR 303 specification. You need to pass in a Validator and a Validator entity, so how to get the Validator is as follows:

@Configuration public class BeanConfigs { @Bean public javax.validation.Validator getValidator(){ return new LocalValidatorFactoryBean(); }}

It will get a Validator object, which we can then inject into the service to use:

 @Autowired     
private Validator validator ;

So how is the BeanValidators class implemented? It’s as simple as judging the JSR 303 annotations.

So where are the annotations for JSR 303? Address entity class: Address entity class: Address entity class

@Entity @Setter @Getter public class Address { @Id @GeneratedValue private Integer id; @NotNull private String province; // save @NotNull private String City; // city @notnull private String county; // private Boolean isDefault = false; @ManyToOne(Cascade ={CascadeType.all}) @JoinColumn() private User User; }

Write the constraints you need to make a judgment, if reasonable, then you can do the business operation, so that the database operation.

This piece of validation is necessary for one primary reason: it avoids dirty data inserts.

If the reader has experience with the launch, he can understand that any code error can be tolerated and fixed, but if there is a problem with dirty data, it can be a devastating disaster. Program problems can be fixed, but the occurrence of dirty data may not be recoverable. This is why it is important to determine the constraints in the Service before performing the business logic operation.

The judgment here is the business logic judgment, which is filtered and judged from the business perspective. In addition, there may be different business conditions constraints in many scenarios, so you just need to do it according to the requirements.

Constraint conditions are summarized as follows:

  • Basic judgment constraints (NULL values and other basic judgments)
  • Entity attribute constraints (meet JSR 303 and other basic judgments)
  • Business condition constraints (different business constraints presented by requirements)

When all three are satisfied, the next step can be taken

OK, basically introduced how to make a basic judgment, so back to the problem of exception design, the above code has clearly described how to reasonably judge an exception in the appropriate place, then how to reasonably throw an exception?

Is throwing a RuntimeException an elegant exception? Of course not. I think there are two ways to throw exceptions in a service:

  • Throws an exception with RumtimeException
  • Throws a RuntimeException of the specified type

The first exception is that all of my exceptions are thrown with a RuntimeException, but with a status code. The caller can use the status code to find out what exception the service threw.

The second type of exception is when a service throws an exception that is specified by a custom exception and then throws the exception again.

In general, if the system has no other special requirements, in the development design, the second method is recommended. But, for example, basic exceptions can be handled entirely using the library provided by Guava. JSR 303 exceptions can also be manipulated using their own wrapped exception judgment classes, because both types of exceptions are basic judgments and no special exceptions need to be specified for them. However, in the case of the third obligation condition, you need to throw an exception of the specified type.

for

Throw new RuntimeException(" Current user cannot be found!") );

Define a specific exception class to determine this obligatory exception:

Public class NotFindUserException extends RuntimeException {public NotFindUserException() {super(" This user could not be found "); } public NotFindUserException(String message) { super(message); }}

Then change this to:

Throw new NotFindUserException(" Current user cannot be found!") ); or throw new NotFindUserException();

OK, with the above modifications to the Service layer, the code changes as follows:

@ Override public Address createAddress (Integer uid, Address the Address) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. CheckNotNull (uid) : The user ID cannot be empty, and the user does exist; User user = userDao.findOne(uid); If (null == user){throw new NotFindUserException(" User not found!"); ); } / / 2. The shipping address of the necessary fields can't be empty BeanValidators. ValidateWithException (validator, address); If (objectutils.isEmpty (user.getaddresses ())){address.setisDefault(true); if(address.getaddresses ()) {address.setisDefault(true); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = address. SetUser (user); Address result = addressDao.save(address); return result; }

This service looks more stable and understandable.

Delete shipping address:

The arguments:

  • The user id
  • Receipt address ID

Constraints:

  • The user ID cannot be empty, and the user does exist
  • The shipping address cannot be empty, and it does exist
  • Determine whether the shipping address is the user’s shipping address
  • Determine whether this shipping address is the default shipping address. If it is the default shipping address, it cannot be deleted

It is similar to adding the shipping address above, so without further elaboration, the service design of DELETE is as follows:

@ Override public void deleteAddress (Integer uid, Integer aid) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. CheckNotNull (uid) : The user ID cannot be empty, and the user does exist; User user = userDao.findOne(uid); if(null == user){ throw new NotFindUserException(); } checkNotNull(aid);} checkNotNull(aid); Address address = addressDao.findOne(aid); if(null == address){ throw new NotFindAddressException(); } //3. If (! address.getUser().equals(user)){ throw new NotMatchUserAddressException(); } / / 4. Determine whether the shipping address as the default shipping address, if this is the default shipping address, so can't be deleted if (address) getIsDefault ()) {throw new DefaultAddressNotDeleteException (); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = addressDao. Delete (address); }

Four related exception classes are designed: NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException. Throw different exceptions according to different business requirements.

Get the list of shipping addresses: Enter parameters:

  • The user id

Constraints:

  • The user ID cannot be empty, and the user does exist

The code is as follows:

@ Override public List < Address > listAddresses (Integer uid) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. CheckNotNull (uid) : The user ID cannot be empty, and the user does exist; User user = userDao.findOne(uid); if(null == user){ throw new NotFindUserException(); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = User result. = userDao findOne (uid); return result.getAddresses(); }

API Exception Design

There are roughly two ways to throw it:

  • Throws an exception with RumtimeException
  • Throws a RuntimeException of the specified type

This is what we mentioned when we designed the service layer exception. By introducing the service layer, we chose the second way to throw an exception. The difference is that we need to use these two ways to throw an exception in the API layer: Specify the type of the API exception and the associated status code before throwing the exception. The core of this exception design is to make it more clear to the user calling the API that the exception occurred.

In addition to throwing exceptions, we also need to make a corresponding table to display the detailed information of the exception corresponding to the status code and the possible problems of the exception, so as to facilitate the user’s query. (such as API documentation provided by GitHub, API documentation provided by WeChat, etc.), there is another benefit: if the user needs to customize the prompt message, the prompt can be modified according to the status code returned.

API validation constraints

First of all, the design of the API requires the existence of a DTO object, which is responsible for the communication and transfer of data to the caller, and then the DTO-> domain to the service for operation. This must be noted.

Second, in addition to the NULL validation and JSR 303 validation mentioned above, the API layer also needs to validate the service, and if the validation fails, it will be sent back directly to the caller to tell him that the call failed. A service should not be accessed with illegal data.

The reader may be confused. If the service is already validated, why should the API layer validate? There is a concept here: Murphy’s law in programming. If the data validation of API layer is neglected, then illegal data may be transferred to the Service layer, and dirty data will be saved to the database.

So the core of careful programming is: never trust the data you receive to be legitimate.

API Exception Design

When designing an API layer exception, as we mentioned above, we need to provide error codes and error messages, so we can design this by providing a generic API superclass exception from which all other API exceptions inherit:

public class ApiException extends RuntimeException { protected Long errorCode ; protected Object data ; public ApiException(Long errorCode,String message,Object data,Throwable e){ super(message,e); this.errorCode = errorCode ; this.data = data ; } public ApiException(Long errorCode,String message,Object data){ this(errorCode,message,data,null); } public ApiException(Long errorCode,String message){ this(errorCode,message,null,null); } public ApiException(String message,Throwable e){ this(null,message,null,e); } public ApiException(){ } public ApiException(Throwable e){ super(e); } public Long getErrorCode() { return errorCode; } public void setErrorCode(Long errorCode) { this.errorCode = errorCode; } public Object getData() { return data; } public void setData(Object data) { this.data = data; }}

Then define API layer exceptions separately: ApiDefaultAddressNotDeleteException,ApiNotFindAddressException,ApiNotFindUserException,ApiNotMatchUserAddressException For example, the default address cannot be deleted:

public class ApiDefaultAddressNotDeleteException extends ApiException { public ApiDefaultAddressNotDeleteException(String message) { super(AddressErrorCode.DefaultAddressNotDeleteErrorCode, message, null); }}

AddressErrorCode. DefaultAddressNotDeleteErrorCode is the need to provide the error code to the caller. The error code class is as follows:

public abstract class AddressErrorCode { public static final Long DefaultAddressNotDeleteErrorCode = 10001L; Public static final Long notFindAddressSerrorCode = 10002L; public static final Long notFindAddressSerrorCode = 10002L; Public static final Long notFindUserErrorCode = 10003L; public static final Long notFindUserErrorCode = 10003L; / / can't find this user public static final Long NotMatchUserAddressErrorCode = 10004 l; // User does not match shipping address}

OK, so the API layer exception has been designed. Just to be clear, the AddresserrorCode error class holds the error code that can occur. It is more reasonable to manage it in a configuration file.

API handles exceptions

The API layer will call the Service layer and handle any exceptions that occur in the Service. First, make sure the API layer is very light and is basically a forwarding function (interface parameters, passed to service parameters, Return data to the caller, these three basic functions), and then exception handling is performed on the method call passed to the service parameter.

Let’s just take adding an address as an example:

@Autowired private IAddressService addressService; @RequestMapping(method = requestMethod.post) public AddressTo add(@Valid) @RequestMapping(method = requestMethod.post @RequestBody AddressDTO addressDTO){ Address address = new Address(); BeanUtils.copyProperties(addressDTO,address); Address result; try { result = addressService.createAddress(addressDTO.getUid(), address); }catch (NotFindUserException e){throw new ApinotFindUserException (" This user cannot be found "); }catch (Exception E){throw new ApiException(E); } AddressDTO resultDTO = new AddressDTO(); BeanUtils.copyProperties(result,resultDTO); resultDTO.setUid(result.getUser().getId()); return resultDTO; }

The solution here is to call a service, determine the type of exception, and then convert any service exception to an API exception, and then throw an API exception. This is a common method of exception conversion. Similarly, deleting the receiving address and obtaining the receiving address are also handled in this way, which will not be described here.

API Abnormal Transformation

We have already explained how to throw an exception and how to convert a Service exception into an API exception, so is throwing an API exception complete? The answer is no, when an API exception is thrown, we need to make the data (JSON or XML) returned by the API exception understood by the user, so we need to convert the API exception into a DTO object (ErrordTo), see the following code:

@ControllerAdvice(annotations = RestController.class) class ApiExceptionHandlerAdvice { /** * Handle exceptions thrown by handlers. */ @ExceptionHandler(value = Exception.class) @ResponseBody public ResponseEntity<ErrorDTO> exception(Exception exception,HttpServletResponse response) { ErrorDTO errorDTO = new ErrorDTO(); If (Exception instanceof ApiException){// API Exception ApiException = (ApiException) Exception; errorDTO.setErrorCode(apiException.getErrorCode()); }else{// Unknown exception errorDot.seterRorCode (0L); } errorDTO.setTip(exception.getMessage()); ResponseEntity<ErrorDTO> responseEntity = new ResponseEntity<>(errorDTO,HttpStatus.valueOf(response.getStatus())); return responseEntity; } @Setter @Getter class ErrorDTO{ private Long errorCode; private String tip; }}

OK, so we’ve completed the conversion of the API exception to a user-readable DTO object, using @ControllerAdvice, which is a special aspect handling provided by Spring MVC.

The user can also receive the normal data format when an exception occurs when calling the API interface. For example, when there is no user (uid 2), but the shipping address is added to the user, the data after postman(Google Plugin is used to simulate HTTP requests) :

{"errorCode": 10003, "tip": "the user could not be found"}

conclusion

This article only focuses on how to design exceptions, and the API transmission and service processing involved need to be optimized. For example, API interface access needs to be encrypted using HTTPS, API interface needs OAuth2.0 authorization or API interface needs signature authentication, etc. Not mentioned in the article, the focus of this article is on how exceptions are handled, so the reader should only focus on the issues related to exceptions and how they are handled.

Recommended reading

Java note daq.md

Amazing, this Java website, everything! https://markerhub.com

The B station of the UP Lord, the JAVA is really good!

Too great! The latest edition of Java programming ideas can be viewed online!