4.2 Attributes
In the mini MVC project, we have used attributes to send data from controllers to views. Attributes, like init parameters, are in the form of name-value. But different from init parameters, the values can be objects, rather than simple strings.
An attribute is like an object pinned to a bulletin board. Somebody stuck it on the board so that others can get it.
To use attributes, we need to understand two questions: 1) Who has access to it?; 2) How long does it live? In other words, what is the scope of the attribute? Generally speaking, there are three scopes for attributes:
- Context: It is the
ServletContext
representing the web application. - Request: It is the request part of a request-response model, and you can simply think of it as the
HttpServletRequest
object in most cases. - Session: This is a key point (
HttpSession
) we will explain in detail later. Now you can simply think of it as several request-response phrases by the same person.
For example,
request.setAttribute("books", books);
RequestDispatcher dispatcher = request.getRequestDispatcher("result.jsp");
dispatcher.forward(request, response);
In the code above, the attribute is in the scope of a request. Because result.jsp
and this servlet use the same request when forwarding, result.jsp
is able to call getAttribute()
to get the attribute.
Attributes vs. parameters
In the last section, we have studied two types parameters: servlet init parameters and context init parameters. In fact, what we send from views to controller are also parameters. We can find many similarities among them: name-value string pairs; only get()
is allowed.
Many students may find it hard to distinguish attributes and parameters. The following table summarizes the main differences.
Attributes | Parameters | |
---|---|---|
Types | Context Request Session | Context init parameters Request parameters Servlet init parameters |
Method to set | setAttribute(String, Object) | You cannot set init parameters[1] |
Return type | Object | String |
Method to get | getAttribute(String) | getInitParameter(String) |
[!TIP] In daily programming, attributes is much more frequently used than init parameters.
In what follows, we will have a glance at attributes related APIs. Readers can refer to the API documentation for more information.
Thread-safety and attributes
We know that there are three scopes of attributes, but there is a dark side that cannot be ignored: thread-safety. In this book, we won't take much space to explain the thread-sate of attributes in a technical view. Rather, we think it can also be well understood through natural language.
A class is thread-safe if it behaves correctly when accessed from multiple threads.
As a matter of fact, thread-safety is a common issue in terms of data sharing. For example, suppose an attribute is in the scope of ServletContext
. It is possible for several servlets/threads to update it at the same time. So context scope is not thread safe. Similarly, session scope is not thread safe either, because there could be more than more request at the same time from the same client (i.e., the same session).
To achieve thread safety, the main idea is to let only thread access the current attribute at a time, and one method is to use synchronized
in Java. As we can imagine, such restriction would result in bad performance in the context scope. So we should not update attributes in the context scope if possible. Luckily, for session scope, it is not a big deal: one person one request at a time. Quite fair, isn't it?
HttpSession session = request.getSession();
synchronized(session) {
session.setAttribute("foo", "22");
session.setAttribute("bar", 42);
}
The code above may look sophistic for beginners. Don't worry. Currently what you need to know is that 1). What thread-safety is; 2) Attributes in session scope is not thread-safe; 3) synchronized
can solve this problem.
A final question: what about request attributes? In Fig 3.3, we know that each request is processed in a single thread. So attributes in request scope is thread-safe!
Attributes example: database connection
The usage of attributes is very straightforward. In this section, let's try to use attributes to implement a practical functionality in web applications.
In last section, we mentioned that we can put database connection string in init parameters. And any database operations in the web application should use this global database connection. It is reasonable to put it in the context attribute.
DatabaseConn conn = new DataBaseConn("hostname", "user", "password", "database");
context.setAttribute("database", conn);
Note that DatabaseConn
above is mock class for testing, and it cannot carry out any real database task.
The next question is: where should the code above be put? What about a servlet's init()
? It seems a bad idea, because we would expect a place where the whole application is initialized (before any servlets or JSPs). Is there a init()
of context for us to override? Unfortunately, no. It seems like a very hard problem. At least servlets are unable to do anything.
But, listeners can solve this problem! The core idea behind listeners is:
- A listener binds to an event.
- When the event is triggered, the listener will be notified and then do something (callback event).
Back to our example. A listener binds to the context's initialization event; when the context is initialized, we call setAttribute()
method.
In Java EE, there are several listener interfaces, and what we need to do is to implement them and override their methods. For example, the one related with context is ServletContextListener
[2].
@WebListener
public class DatabaseListener implements ServletContextListener
Again, we use annotations here to declare that it is a listener. Readers can try out how to do it alternatively in DD.
As we can this, this interface binds two events, representing context's initialization and destroying, respectively. So what is ServletContextEvent
? This is the event class for notifications about changes to the servlet context of a web application, and it has a reference to ServletContext
.
In the contextInitialized()
method, we can implement our callback logic, and the complete code can be found at DatabaseListener.java
of ch4/listener
.
DatabaseConn conn = new DatabaseConn(host, user, password, database);
sce.getServletContext().setAttribute("database", conn);
Then we can use the sharing conn
in servlets:
DatabaseConn conn = (DatabaseConn) getServletContext().getAttribute("database");
Besides context events, you can listen for events related to context attributes, servlet requests and attributes, and HTTP sessions and session attributes. Again, you don't have to memorize them. The key point is to understand how a listener works, and also remember: there are many lifecycle events in a web application, and it is possible to be notified and do something using a right type of listener.
- ServletRequestListener
- HttpSessionAttributeListener
- HttpSessionBindingListener
- ServletContextAttributeListener
- HttpSessionListener
- ServletRequestAttributeListener
Listener example: log
Many websites would log every request information containing who was visiting the site, where they came from, and exactly what they were doing on the web site, and these log files are valuable resources for business intelligence. The W3C maintains a standard format for web server log files, and for simplicity, we design a compact format based on it. Each line is in the form of time method resource, separately by a blank space. For example,
00:34:23.344207 GET /foo/bar.html
12:21:16.963480 POST /foo/a
12:45:52.255619 GET /foo/b.jsp
To accomplish this task, we should listen to the event of every request coming in, and that is exactly what ServletRequestListener does.
@WebListener
public class LogListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
// log
}
}
Essentially, logs are append-only files. Firstly, we need to get the current time and necessary information from sre
:
String now = LocalTime.now().toString();
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String method = request.getMethod();
String context = request.getContextPath();
String path = request.getServletPath();
String s = "%s %s %s%s\n";
String record = String.format(s, now, method, context, path);
Then we use Files
to write this line to the file[3], and it is thread-safe.
Path p = Path.of("/Users/zhongpu/Desktop/file.log");
try {
Files.writeString(p, record, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
e.printStackTrace();
}
By the way, due to the importance of logging, communities have designed some awesome specialized libraries (e.g., SLF4J) to help programmers to deal with logging. We also develop another logging system using the built-in java.util.logging
, and the complete code can be found at LogListener.java
of ch3/listener
.
[1] With request parameters, you can adjust the query String, but that's different.
[2] In IntelliJ IDEA, when we create a listener, it will automatically implement ServletContextListener
, HttpSessionListener
, HttpSessionAttributeListener
. You can delete unneeded interfaces and their methods.
[3] The language level should be set to at least 11.