It's a title like "I tried dancing", but it's a serious article.
A while ago, I researched JMX in a project I participated in, and I thought that if I used this, I wouldn't have to create a setting screen for simple settings. I think it's hard to recommend to customers, but I thought that it would be possible to reduce the man-hours for creating setting screens for in-house products, so I made an app that can manage login information easily and looked at the usability.
This time it is a simple function, so I do not use DB. I used Spring to create a simple web application that just logs in. The libraries used are as follows.
I will briefly describe the specifications of login management.
--You can browse the list of logged-in users. --Maximum number of logins can be set. --ID lock can be implemented.
This is enough for the login management function. It's annoying to implement too many features.
I created a simple web application that just logs in. The implementation is a very common web application using Spring MVC. It is implemented so that the logged-in information can be viewed with JMX.
I have created an MBean to manage login information. First, create the interface.
public interface LoginMonitorMBean {
public static final String NAME = "examples.jmx:type=LoginMonitoring";
public static final String LUI_ITEM_ID = "id";
public static final String LUI_ITEM_NAME = "name";
public int getLoginCount();
public CompositeData[] getLoginInfos();
void addLoginInfo(CompositeData loginUserInfo);
public void removeLoginInfo(int id);
public void resetLoginInfo();
public int getMaxLoginCount();
public void setMaxLoginCount(int count);
public int[] getLoginLockIds();
public void addLoginLockId(int id);
public void removeLoginLockId(int id);
public void resetLoginLockId();
public static ObjectName createObjectName() {
try {
return new ObjectName(LoginMonitorMBean.NAME);
} catch (MalformedObjectNameException e) {
throw new IllegalArgumentException(e);
}
}
}
It seems that you must always add MBean to the end of the interface name. Ignore constants and static methods. It is a group of methods that other methods can be operated from jconsole. A description of each method is given below.
Method name | Description |
---|---|
getLoginCount | You can get the number of logins. |
getLoginInfos | You can get the ID and name of the logged-in user. |
addLoginInfo | You can add login information. |
removeLoginInfo | Deletes the login information of the specified ID. |
resetLoginInfo | Reset your login information. |
getMaxLoginCount | You can get the maximum number of logins. |
setMaxLoginCount | You can set the maximum number of logins. |
getLoginLockIds | You can get an array of locked IDs. |
addLoginLockId | You can add an ID to lock. |
removeLoginLockId | Delete the locked ID. |
resetLoginLockId | Reset the locked ID. |
Create a class that implements the interface you just created.
public class LoginMonitor implements LoginMonitorMBean {
}
An error will occur if the management class does not have a class name that excludes MBean from the interface name. Although the implementation content is omitted, it is basically a simple implementation that holds and acquires login information, lock ID, etc. in the class field.
Register the MBean that holds the login information. Registration is done when the Web application is started.
public class StartupBean {
private static Logger log = LoggerFactory.getLogger(StartupBean.class);
@PostConstruct
public void initAfterStartup() {
try {
log.info("MBean registration process");
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
mbs.registerMBean(new LoginMonitor(), LoginMonitorMBean.createObjectName());
} catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
throw new IllegalStateException(e);
}
}
}
Data contents cannot be viewed from jconsole in data classes (DTO-like classes) that implement general setters / getters.
Use javax.management.openmbean.CompositeData
to make it viewable from jconsole etc.
Let's look at an example of retaining login information.
public class LoginUserInfo {
private static final String ID = LoginMonitorMBean.LUI_ITEM_ID;
private static final String NAME = LoginMonitorMBean.LUI_ITEM_NAME;
private final int id;
private final String name;
public LoginUserInfo(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void addMBean(LoginMonitorMBean mbean) throws OpenDataException {
CompositeType compositeType = new CompositeType(
"LoginUserInfo",
"Data type that holds login user information",
new String[] { ID, NAME },
new String[] { "Login user ID", "Login username" },
new OpenType[] { SimpleType.INTEGER, SimpleType.STRING });
Map<String, Object> dataMap = new HashMap<>();
dataMap.put(ID, id);
dataMap.put(NAME, name);
mbean.addLoginInfo(new CompositeDataSupport(compositeType, dataMap));
}
public static int getMBeanId(CompositeData data) {
return (Integer) data.get(ID);
}
}
This is an example where the addMBeanmethod registers the data of
javax.management.openmbean.CompositeData. This
LoginUserInfois a simple data class just to hold common login IDs and login usernames. The ID used at login and the user name obtained from the database etc. (fixed value because this is a simple application) are retained. The information in this class is converted to
javax.management.openmbean.CompositeDataSupport, which is an implementation class of the
javax.management.openmbean.CompositeData` interface.
By doing this, you can browse from jconsole as follows.
The login function is simple.
After logging in, the MBean managed by JMX is taken out, the login limit and lock ID are checked, and if there is no error, the login information is registered in the MBean.
The implemented LoginController
is as follows.
@Controller
public class LoginController {
@PostMapping("/login")
public String login(@Validated @ModelAttribute LoginForm form, BindingResult result, Model model) {
//Input error check
if (result.hasErrors()) {
model.addAttribute("validationError", "Input error");
return "login";
}
//Login check
if (100 > form.getLoginId() && 300 < form.getLoginId()) {
model.addAttribute("validationError", "Login error");
return "login";
}
if (!"testtest".equals(form.getLoginPasswd())) {
model.addAttribute("validationError", "Login error");
return "login";
}
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
LoginMonitorMBean mbean = JMX.newMBeanProxy(mbs, LoginMonitorMBean.createObjectName(), LoginMonitorMBean.class);
//Login number check
if (mbean.getMaxLoginCount() > 0 && mbean.getLoginCount() >= mbean.getMaxLoginCount()) {
model.addAttribute("validationError", "Login number limit");
return "login";
}
//ID lock check
if (Arrays.stream(mbean.getLoginLockIds()).filter(lockId -> lockId == form.getLoginId()).findFirst()
.isPresent()) {
model.addAttribute("validationError", "ID locked");
return "login";
}
//Login information registration
try {
LoginUserInfo info = new LoginUserInfo(form.getLoginId(),
String.format("Test user (%d)", form.getLoginId()));
info.addMBean(mbean);
} catch (OpenDataException e) {
e.printStackTrace();
model.addAttribute("validationError", "Internal system error");
return "login";
}
model.addAttribute("loginCount", mbean.getLoginCount());
return "home";
}
}
Since this web application is Spring Boot, it can be started like the following.
java -jar jmx-examples-1.0.0.war
In order to check the registered MBean remotely, it is necessary to set the startup parameters as follows.
java -Dcom.sun.management.jmxremote.port=5000 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar jmx-examples-1.0.0.war
Startup parameters | Description |
---|---|
com.sun.management.jmxremote.port | You can specify the port number of the MBean server. |
com.sun.management.jmxremote.authenticate | This time, because it is a simple function, the authentication function when accessing the MBean server is disabled. |
com.sun.management.jmxremote.ssl | This time, for the sake of simplicity, SSL is disabled when accessing the MBean server. |
If you start it as above, you can access it by remote process from jconsole etc.
It can be accessed from both local and remote processes from jconsole, but I tried to create a client application that accesses the process ID and remotely in the same way.
Refer to the following page for how to connect JMX. 2 Monitoring and Management Using JMX Technology
In order to access the MBean server remotely, you need to specify the startup parameters for com.sun.management.jmxremote.port
.
The following address is generated using the port number specified in the above startup parameter.
service:jmx:rmi:///jndi/rmi://localhost:5000/jmxrmi
Then use the above address to connect to the MBean server.
String connectAddress = "service:jmx:rmi:///jndi/rmi://localhost:5000/jmxrmi";
try (JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(connectAddress))) {
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
LoginMonitorMBean lmbean = JMX.newMBeanProxy(mbsc, LoginMonitorMBean.createObjectName(),
LoginMonitorMBean.class);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
A process ID is required for local access. If you know the process ID, you can connect, so you do not need to specify the startup parameter.
String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
VirtualMachine vm = VirtualMachine.attach(pid);
String connectorAddress;
try {
connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
if (connectorAddress == null) {
vm.startLocalManagementAgent();
connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
}
} finally {
vm.detach();
}
Since the character string for connection can be obtained with the above logic, it is possible to connect to the local process with the following description described even for remote connection using that character string.
JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(connectAddress))
By connecting to the MBean server, you can get the JVM information in addition to the registered MBean. The MBean for acquiring the virtual machine name and process ID and the MBean for acquiring heap information can be acquired with the following description.
try (JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(connectAddress))) {
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
//Display runtime information for the Java virtual machine
RuntimeMXBean rmxbean = ManagementFactory.getPlatformMXBean(mbsc, RuntimeMXBean.class);
//Get process ID etc. from rmx bean
//Display memory information
MemoryMXBean mmxbean = ManagementFactory.getPlatformMXBean(mbsc, MemoryMXBean.class);
MemoryUsage memoryUsage = mmxbean.getHeapMemoryUsage();
//Get heap information from memoryUsage
//Display OS information
OperatingSystemMXBean omxbean = ManagementFactory.getPlatformMXBean(mbsc, OperatingSystemMXBean.class);
//Get OS information from omxbean
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
When I tried using JMX, it was so easy and convenient that I didn't need to create a setting screen with only one text box. Of course, if you have performance problems and setting information in the DB, you may consider whether you can connect to the DB and how much the load is, but you can easily change the settings with jconsole, so the man-hours to create the screen I felt that it could be reduced. The application created this time is stored in the following repository, so please have a look.
Recommended Posts