A Swing JLabel automatically interprets any text as HTML content, if it starts with <html>. If the content of this HTML is an image with invalid URL this will cause the whole GUI to hang since the ImageFetche which sho开发者_如何学Culd load this image will quit by an NPE.
To reproduce this problem simply create a JLabel as follows
new JLabel("<html><img src='http:\\\\invalid\\url'>")
I know there is a client property to prevent the JLabel from interpreting HTML. But JLabel is the default renderer implementation for many Swing components(like JTree, JTable and so on) which makes this a problem for nearly any Swing application which allows user input. So instead of implementing tons of custom renderer I'm searching for a global solution to disable the HTML interpretation.
There is a way if you create your own look and feel.
I'm not sure how well this performs is this, but it works. Lets assume you will extend the "Classic Windows" L&F.You need at leas 2 classes
One is the Look&Feel itself, lets call it WindowsClassicLookAndFeelExt. You only need to override method initClassDefaults.
package testSwing;
import javax.swing.UIDefaults;
import com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel;
public class WindowsClassicLookAndFeelExt extends WindowsClassicLookAndFeel {
@Override protected void initClassDefaults(UIDefaults table){
super.initClassDefaults(table);
Object[] uiDefaults = { "LabelUI", WindowsLabelExtUI.class.getCanonicalName()};
table.putDefaults(uiDefaults);
}
}
You also need a WindowsLabelExtUI class to manage all JLabels and set the property:
package testSwing;
import javax.swing.JComponent;
import javax.swing.plaf.ComponentUI;
import com.sun.java.swing.plaf.windows.WindowsLabelUI;
public class WindowsLabelExtUI extends WindowsLabelUI{
static WindowsLabelExtUI singleton = new WindowsLabelExtUI();
public static ComponentUI createUI(JComponent c){
c.putClientProperty("html.disable", Boolean.TRUE);
return singleton;
}
}
And finally a test class when you set the theme as WindowsClassicLookAndFeelExt
package testSwing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.UIManager;
public class Main{
public static void main(String[] args){
try{ UIManager.setLookAndFeel(WindowsClassicLookAndFeelExt.class.getCanonicalName());
}catch (Exception e){
e.printStackTrace();
}
JFrame frame = new JFrame("JList Test");
frame.setLayout(new FlowLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
String[] selections = {"<html><img src='http:\\\\invalid\\url'>", "<html><H1>Hello</h1></html>", "orange", "dark blue"};
JList list = new JList(selections);
list.setSelectedIndex(1);
System.out.println(list.getSelectedValue());
JLabel jLabel = new JLabel("<html><h2>standard Label</h2></html>");
frame.add(new JScrollPane(list));
frame.add(jLabel);
frame.pack();
frame.setVisible(true);
}
}
And you will see something like
For a simple JLabel, you can call the JComponent method
myLabel.putClientProperty("html.disable", Boolean.TRUE);
on the label where you want to disable HTML rendering.
Reference: Impossible to disable HTML Rendering in a JLabel
For something like a JTable, JTree, or JList you'll need to create a custom cell renderer that sets this property. Here's an example (modified from this example) that creates a custom cell renderer for a JList.
import java.awt.Component;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
public class JListTest {
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame frame = new JFrame("JList Test");
frame.setLayout(new FlowLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
String[] selections = { "<html><img src='http:\\\\invalid\\url'>",
"red", "orange", "dark blue" };
JList list = new JList(selections);
// set the list cell renderer to the custom class defined below
list.setCellRenderer(new MyCellRenderer());
list.setSelectedIndex(1);
System.out.println(list.getSelectedValue());
frame.add(new JScrollPane(list));
frame.pack();
frame.setVisible(true);
}
}
class MyCellRenderer extends JLabel implements ListCellRenderer {
public MyCellRenderer() {
setOpaque(true);
putClientProperty("html.disable", Boolean.TRUE);
}
public Component getListCellRendererComponent(
JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus)
{
setText(value.toString());
return this;
}
}
I used the example code from the ListCellRenderer documentation as a starting point for the custom list cell renderer.
When I run the example, you can see that the HTML in the first list entry is rendered instead of being interpreted.
Since there is no way of globally setting the html.disable
property to true for each created JLabel
, one hacky way (I say hacky because I'm not sure of the impact on performance, or whether such a solution could be placed on production) is to do some bytecode interception for every created JLabel
instance. A library like ByteBuddy can do this. I've experimented a bit with ByteBuddy and found a way to set a Java agent that intercepts calls to the setText()
method for a JLabel
. This method is called when creating a JLabel
with the provided text.
Agent
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.AgentBuilder.InitializationStrategy;
import net.bytebuddy.agent.builder.AgentBuilder.Listener;
import net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy;
import net.bytebuddy.agent.builder.AgentBuilder.TypeStrategy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.matcher.StringMatcher;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.description.type.TypeDescription.ForLoadedType;
import static net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader.read;
import static net.bytebuddy.dynamic.loading.ClassInjector.UsingInstrumentation.Target.BOOTSTRAP;
import static net.bytebuddy.matcher.ElementMatchers.*;
public class JLabelAgent {
private static final Class<?> INTERCEPTOR_CLASS = JLabelInterceptor.class;
private JLabelAgent() {
}
public static void premain(String arg, Instrumentation instrumentation) throws Exception {
injectBootstrapClasses(instrumentation);
new AgentBuilder.Default()
.with(RedefinitionStrategy.RETRANSFORMATION)
.with(InitializationStrategy.NoOp.INSTANCE)
.with(TypeStrategy.Default.REDEFINE)
.ignore(new AgentBuilder.RawMatcher.ForElementMatchers(nameStartsWith("net.bytebuddy.").or(isSynthetic()), any(), any()))
.with(new Listener.Filtering(
new StringMatcher("javax.swing.JLabel", StringMatcher.Mode.EQUALS_FULLY),
Listener.StreamWriting.toSystemOut()))
.type(named("javax.swing.JLabel"))
.transform((builder, type, classLoader, module) ->
builder.visit(Advice.to(INTERCEPTOR_CLASS).on(named("setText")))
)
.installOn(instrumentation);
}
private static void injectBootstrapClasses(Instrumentation instrumentation) throws IOException {
File temp = Files.createTempDirectory("tmp").toFile();
temp.deleteOnExit();
ClassInjector.UsingInstrumentation.of(temp, BOOTSTRAP, instrumentation)
.inject(singletonMap(new ForLoadedType(INTERCEPTOR_CLASS), read(INTERCEPTOR_CLASS)));
}
}
Interceptor
import javax.swing.JComponent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.Advice.Argument;
import net.bytebuddy.asm.Advice.This;
public class JLabelInterceptor {
@Advice.OnMethodEnter()
public static void setText(@This Object label, @Argument(0) String text) {
((JComponent) label).putClientProperty("html.disable", Boolean.TRUE);
System.out.println("Label text is " + text);
}
}
Example
public static void main(String[] args) throws Exception {
JFrame frame = new JFrame("JList Test");
frame.setLayout(new FlowLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
String[] selections = {"<html><img src='http:\\\\invalid\\url'>", "<html><H1>Hello</h1></html>", "orange", "dark blue"};
JList list = new JList(selections);
list.setSelectedIndex(1);
System.out.println(list.getSelectedValue());
JLabel jLabel = new JLabel("<html><h2>standard Label</h2></html>");
frame.add(new JScrollPane(list));
frame.add(jLabel);
frame.pack();
frame.setVisible(true);
}
Running the example
Compile the Java agent then run the example:
java -javaagent:agent.jar -jar example.jar
Note: When using Maven to build the agent Jar, I had to put the following configuration in the POM to setup the manifest:
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Agent-Class>example.JLabelAgent</Agent-Class>
<Premain-Class>example.JLabelAgent</Premain-Class>
<Boot-Class-Path>byte-buddy-1.10.14.jar</Boot-Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
Hanging is probably the least unpleasant behavior. This is why Data Validation is so very important. Just do not allow the users to enter something like that.
精彩评论