Java compress / obfuscate JavaScript code

Basically, they are all tools written by themselves to build front-end projects, and tools to compress / obfuscate JavaScript code are essential. We are Java platform, that is to say, using java to compress JS is more convenient. Although we can call special front-end building tools such as node externally, it's not easy. It can be done in the Java community. We don't make it too complicated. Good ~ I don't talk much. Let's see the low configuration version first.

Low configuration version

This low configuration version is composed of several functions. It's called "low configuration version" because it doesn't come from other third-party packages before. It's easy and practical. I've used it for a long time.

/**
 * This file is part of the Echo Web Application Framework (hereinafter "Echo").
 * Copyright (C) 2002-2009 NextApp, Inc.
 *
 * Compresses a String containing JavaScript by removing comments and
 * whitespace.
 */
public class JavaScriptSimpleCompressor {
	private static final char LINE_FEED = '\n';
	private static final char CARRIAGE_RETURN = '\r';
	private static final char SPACE = ' ';
	private static final char TAB = '\t';

	/**
	 * Compresses a String containing JavaScript by removing comments and
	 * whitespace.
	 * 
	 * @param script the String to compress
	 * @return a compressed version
	 */
	public static String compress(String script) {
		JavaScriptSimpleCompressor jsc = new JavaScriptSimpleCompressor(script);
		return jsc.outputBuffer.toString();
	}

	/** Original JavaScript text. */
	private String script;

	/**
	 * Compressed output buffer. This buffer may only be modified by invoking the
	 * <code>append()</code> method.
	 */
	private StringBuffer outputBuffer;

	/** Current parser cursor position in original text. */
	private int pos;

	/** Character at parser cursor position. */
	private char ch;

	/** Last character appended to buffer. */
	private char lastAppend;

	/** Flag indicating if end-of-buffer has been reached. */
	private boolean endReached;

	/** Flag indicating whether content has been appended after last identifier. */
	private boolean contentAppendedAfterLastIdentifier = true;

	/**
	 * Creates a new <code>JavaScriptCompressor</code> instance.
	 * 
	 * @param script
	 */
	private JavaScriptSimpleCompressor(String script) {
		this.script = script;
		outputBuffer = new StringBuffer(script.length());
		nextChar();

		while (!endReached) {
			if (Character.isJavaIdentifierStart(ch)) {
				renderIdentifier();
			} else if (ch == ' ') {
				skipWhiteSpace();
			} else if (isWhitespace()) {
				// Compress whitespace
				skipWhiteSpace();
			} else if ((ch == '"') || (ch == '\'')) {
				// Handle strings
				renderString();
			} else if (ch == '/') {
				// Handle comments
				nextChar();
				if (ch == '/') {
					nextChar();
					skipLineComment();
				} else if (ch == '*') {
					nextChar();
					skipBlockComment();
				} else {
					append('/');
				}
			} else {
				append(ch);
				nextChar();
			}
		}
	}

	/**
	 * Append character to output.
	 * 
	 * @param ch the character to append
	 */
	private void append(char ch) {
		lastAppend = ch;
		outputBuffer.append(ch);
		contentAppendedAfterLastIdentifier = true;
	}

	/**
	 * Determines if current character is whitespace.
	 * 
	 * @return true if the character is whitespace
	 */
	private boolean isWhitespace() {
		return ch == CARRIAGE_RETURN || ch == SPACE || ch == TAB || ch == LINE_FEED;
	}

	/**
	 * Load next character.
	 */
	private void nextChar() {
		if (!endReached) {
			if (pos < script.length()) {
				ch = script.charAt(pos++);
			} else {
				endReached = true;
				ch = 0;
			}
		}
	}

	/**
	 * Adds an identifier to output.
	 */
	private void renderIdentifier() {
		if (!contentAppendedAfterLastIdentifier)
			append(SPACE);
		append(ch);
		nextChar();
		while (Character.isJavaIdentifierPart(ch)) {
			append(ch);
			nextChar();
		}
		contentAppendedAfterLastIdentifier = false;
	}

	/**
	 * Adds quoted String starting at current character to output.
	 */
	private void renderString() {
		char startCh = ch; // Save quote char
		append(ch);
		nextChar();
		while (true) {
			if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN) || (endReached)) {
				// JavaScript error: string not terminated
				return;
			} else {
				if (ch == '\\') {
					append(ch);
					nextChar();
					if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN) || (endReached)) {
						// JavaScript error: string not terminated
						return;
					}
					append(ch);
					nextChar();
				} else {
					append(ch);
					if (ch == startCh) {
						nextChar();
						return;
					}
					nextChar();
				}
			}
		}
	}

	/**
	 * Moves cursor past a line comment.
	 */
	private void skipLineComment() {
		while ((ch != CARRIAGE_RETURN) && (ch != LINE_FEED)) {
			if (endReached) {
				return;
			}
			nextChar();
		}
	}

	/**
	 * Moves cursor past a block comment.
	 */
	private void skipBlockComment() {
		while (true) {
			if (endReached) {
				return;
			}
			if (ch == '*') {
				nextChar();
				if (ch == '/') {
					nextChar();
					return;
				}
			} else
				nextChar();
		}
	}

	/**
	 * Renders a new line character, provided previously rendered character is not a
	 * newline.
	 */
	private void renderNewLine() {
		if (lastAppend != '\n' && lastAppend != '\r') {
			append('\n');
		}
	}

	/**
	 * Moves cursor past white space (including newlines).
	 */
	private void skipWhiteSpace() {
		if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
			renderNewLine();
		} else {
			append(ch);
		}
		nextChar();
		while (ch == LINE_FEED || ch == CARRIAGE_RETURN || ch == SPACE || ch == TAB) {
			if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
				renderNewLine();
			}
			nextChar();
		}
	}
}

There is no logic error in the compressed js, otherwise I will not use it for that long. It's just a little bit of egg ache. The goods actually deal with all the spaces in Stirng. Because when writing the vue template, I used multiple lines of strings, and the newline character was \, ha ha, a bit of high-level reference. The low configuration version can't be solved, and it's not your fault, it's not a big crime, just how many lines of code.

The call method is as follows:

JavaScriptSimpleCompressor.compress(jsCode);

Specific compression process:

package com.ajaxjs.web;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.Objects;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Package js
 */
@WebServlet("/JsController")
public class JsController extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String js = "// build date:" + new Date() + "\n";
		js += JavaScriptCompressor.compress(read(mappath(request, "js/ajaxjs-base.js"))) + "\n";
		js += JavaScriptCompressor.compress(read(mappath(request, "js/ajaxjs-list.js"))) + "\n";
		js += action(mappath(request, "js/widgets/"), true) + "\n";

		String output = request.getParameter("output"); // Save in
		Objects.requireNonNull(output, "Required parameters");
		save(output + "\\WebContent\\asset\\js\\all.js", js);
		response.getWriter().append("Pack js Okay.");
	}

	static String frontEnd = "C:\\project\\wstsq\\WebContent\\asset\\css";

	/**
	 * Compress CSS and save it in one place
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String css = request.getParameter("css"),
				file = request.getParameter("file") == null ? "main" : request.getParameter("file");
		String output = "";
		String saveFolder = request.getParameter("saveFolder") == null ? frontEnd : request.getParameter("saveFolder");

		Logger.getGlobal().info(request.getParameter("saveFolder"));

		try {
			save(saveFolder + "\\" + file, css);

			output = "{\"isOk\":true}";
		} catch (Throwable e) {
			e.printStackTrace();
			output = "{\"isOk\":false}";
		}

		response.getWriter().append(output);
	}

	/**
	 * Package all js in a directory
	 * 
	 * @param _folder
	 * @param isCompress
	 * @return
	 */
	public static String action(String _folder, boolean isCompress) {
		StringBuilder sb = new StringBuilder();
		File folder = new File(_folder);
		File[] files = folder.listFiles();

		if (files != null)
			for (File file : files) {
				if (file.isFile()) {
					String jsCode = null;
					try {
						jsCode = read(file.toPath());
					} catch (IOException e) {
						e.printStackTrace();
					}
					sb.append("\n");
					sb.append(isCompress ? JavaScriptCompressor.compress(jsCode) : jsCode);
				}
			}

		return sb.toString();
	}

	/**
	 * Get the real address of the disk
	 * 
	 * @param cxt          Web context
	 * @param relativePath Relative address
	 * @return Absolute address
	 */
	public static String mappath(HttpServletRequest request, String relativePath) {
		String absolute = request.getServletContext().getRealPath(relativePath);

		if (absolute != null)
			absolute = absolute.replace('\\', '/');
		return absolute;
	}

	public static String read(Path path, Charset encode) throws IOException {
		if (Files.isDirectory(path))
			throw new IOException("parameter fullpath: " + path.toString() + " Cannot be a directory, please specify a file");

		if (!Files.exists(path))
			throw new IOException(path.toString() + " non-existent");

		return new String(Files.readAllBytes(path), encode);
	}

	public static String read(String fullpath, Charset encode) throws IOException {
		Path path = Paths.get(fullpath);
		return read(path, encode);
	}

	public static String read(Path path) throws IOException {
		return read(path, StandardCharsets.UTF_8);
	}

	public static String read(String fullpath) throws IOException {
		return read(fullpath, StandardCharsets.UTF_8);
	}

	public static void saveClassic(String fullpath, String content) throws IOException {
		File file = new File(fullpath);
		if (file.isDirectory())
			throw new IOException("parameter fullpath: " + fullpath + " Cannot be a directory, please specify a file");

		try (FileOutputStream fop = new FileOutputStream(file)) {
			if (!file.exists())
				file.createNewFile();

			fop.write(content.getBytes());
			fop.flush();
		}
	}

	public void test() throws IOException {
		String content = read("c://temp//newfile.txt");
		save("c://temp//newfile2.txt", content);
	}

	public static void save(String fullpath, String content) throws IOException {
		Path path = Paths.get(fullpath);

		if (Files.isDirectory(path))
			throw new IOException("parameter fullpath: " + fullpath + " Cannot be a directory, please specify a file");

		if (!Files.exists(path))
			Files.createFile(path);

		Logger.getGlobal().info(path.toString());
		Files.write(path, content.getBytes());

	}
}

YUI Compressor

So we have to use a third-party library. I thought of YUI Compressor for the first time, which I had in my preschool years ("prehistory"), but I'm sorry that I didn't support the arrow function of ES5 and reported errors directly. If you can ignore Error, it's OK - but obviously it's not compatible with the new syntax and can't be compressed out, so I have to give up the bird. Alas, I can't keep up with the situation. In the last 14 years, the update stayed at 2.4.8 you can't love without supporting new JS.

Simple usage is as follows.

private static String yuicompressor(String code) {
	String result = null;

	try (StringWriter writer = new StringWriter();
			InputStream in = new ByteArrayInputStream(code.getBytes());
			Reader reader = new InputStreamReader(in);) {
		JavaScriptCompressor compressor = new JavaScriptCompressor(reader, e);
		compressor.compress(writer, -1, true, false, false, false);
		result = writer.toString();
	} catch (EvaluatorException | IOException e) {
		e.printStackTrace();
	}
	return result;
}

private static ErrorReporter e = new ErrorReporter() {
	@Override
	public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) {
		if (line < 0)
			System.err.println("/n[WARNING] " + message);
		else
			System.err.println("/n[WARNING] " + line + ':' + lineOffset + ':' + message);
	}

	@Override
	public void error(String message, String sourceName, int line, String lineSource, int lineOffset) {
		if (line < 0)
			System.err.println("/n[ERROR] " + message);
		else
			System.err.println("/n[ERROR] " + line + ':' + lineOffset + ':' + message);
	}

	@Override
	public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource,
			int lineOffset) {
		error(message, sourceName, line, lineSource, lineOffset);
		return new EvaluatorException(message);
	}
};

You can also compress CSS.

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;

import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;

import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

/**
 * JS,CSS Compression tool https://blog.csdn.net/jiangguan/article/details/80202559
 * 
 * @author jianggujin
 *
 */
public class CompressorUtils {

	public void compressJS(File js, Writer out) throws Exception {
		compressJS(js, out, -1, true, true, false, false);
	}

	public void compressJS(File js, Writer out, int linebreakpos, boolean munge, boolean verbose, boolean preserveAllSemiColons, boolean disableOptimizations) throws IOException {
		try (InputStreamReader in = new InputStreamReader(new FileInputStream(js), "UTF-8");) {
			JavaScriptCompressor compressor = new JavaScriptCompressor(in, new ErrorReporter() {
				@Override
				public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) {
					System.err.println("[ERROR] in " + js.getAbsolutePath() + line + ':' + lineOffset + ':' + message);
				}

				@Override
				public void error(String message, String sourceName, int line, String lineSource, int lineOffset) {
					System.err.println("[ERROR] in " + js.getAbsolutePath() + line + ':' + lineOffset + ':' + message);
				}

				@Override
				public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource, int lineOffset) {
					error(message, sourceName, line, lineSource, lineOffset);
					return new EvaluatorException(message);
				}
			});

			compressor.compress(out, linebreakpos, munge, verbose, preserveAllSemiColons, disableOptimizations);
		}
	}

	public void compressCSS(File css, Writer out) throws Exception {
		compressCSS(css, out, -1);
	}

	public void compressCSS(File css, Writer out, int linebreakpos) throws IOException {
		try (InputStreamReader in = new InputStreamReader(new FileInputStream(css), "UTF-8");) {
			CssCompressor compressor = new CssCompressor(in);

			compressor.compress(out, linebreakpos);
		}
	}
}

High configuration version - Google close compiler

Your boss is still your boss, Google has been updating this project. In fact, Java ecological js compression tool has no good choice, only master. If you don't say much, give Maven coordinates first.

<!-- https://mvnrepository.com/artifact/com.google.javascript/closure-compiler -->
<dependency>
	<groupId>com.google.javascript</groupId>
	<artifactId>closure-compiler</artifactId>
	<version>v20200504</version>
</dependency>

The problem with Google close compiler is that there are not enough documents to describe the usage in Java. Some of them are out of date. Good thing to find This post , can be compressed smoothly. There's also Closure e-book.

Usage:

/**
 * Verify js syntax and compress js
 * 
 * @param code
 * @return
 */
public static String compileJs(String code) {
	CompilerOptions options = new CompilerOptions();
	// Simple mode is used here, but additional options could be set, too.
	CompilationLevel.WHITESPACE_ONLY.setOptionsForCompilationLevel(options);

	// To get the complete set of externs, the logic in
	// CompilerRunner.getDefaultExterns() should be used here.
	SourceFile extern = SourceFile.fromCode("externs.js", "function alert(x) {}");

	// The dummy input name "input.js" is used here so that any warnings or
	// errors will cite line numbers in terms of input.js.
//		SourceFile input = SourceFile.fromCode("input.js", code);

	SourceFile jsFile = SourceFile.fromFile(code);

	Compiler compiler = new Compiler();
	compiler.compile(extern, jsFile, options);

	// The compiler is responsible for generating the compiled code; it is not
	// accessible via the Result.
	if (compiler.getErrorCount() > 0) {
		StringBuilder sb = new StringBuilder();
		for (JSError jsError : compiler.getErrors()) {
			sb.append(jsError.toString());
		}

		// System.out.println(sb.toString());
	}
	
	return compiler.toSource();
}

Method in phase: Result compile(JSSourceFile extern, JSSourceFile input, CompilerOptions options). Input and options are easy to understand. What is extern? In fact, the description of the class also mentions the following:

External variables are declared in 'externs' files. For instance, the file may include definitions for global javascript/browser objects such as window, document.

Obviously there can be no extern, but it can't be null.

It is mentioned that:

Three compression modes

  • Whitespace only: it's just a simple way to get rid of space wrapping comments.
  • Simple: a little higher than Whitespace only, on the basis of which, the variable names of local variables are also shortened. This is also the compression method used by other compression tools, such as UglifyJS, which is also the most mainstream compression method. It's safer.
  • Advanced: advanced level compression changes (destroys) the original code structure, directly outputs the final running results of the code, and this level of compression will also delete the function code that has not been called

Note: although Advanced level compression achieves the ultimate goal for code compression, it also changes (destroys) the original code structure and directly outputs the final running result of the code. Therefore, it should be used with extreme care. If there is a little irregularity, it may cause compression error or fail to run normally after compression is successful.

I don't know why the simplest Whitespace only still has' use strict '; the official online example doesn't. I had to force a replacement all.

Reference resources

Tags: Programming Java Javascript Google Vue

Posted on Sun, 10 May 2020 04:54:43 -0700 by daijames