出于一些奇怪的原因,尝试了一把MATLAB混合编程。
Java等语言更适合搭建Web Server,处理文件,而MATLAB有许多优秀的内置函数,用起来十分方便。
两者结合,就能写出提供一些计算函数的后端服务,配合上前端,就能在网页上用MATLAB了。

简介

使用其他编程语言调用MATLAB,有两种办法,一种是使用MATLAB ENGINE的API,另一种是MATLAB COMPILER将MATLAB函数打包成其他语言的形式。
我主要使用第二种,原因是在部署机上没有MATLAB的LICENSE,而且也不想在每个Server上都运行一个完整的MATLAB。第二种方式,最终执行函数的机器,
只需要安装了MCR(MATLAB COMPILER RUNTIME)就可以了。

MATLAB COMPILER RUNTIME

在官网上下载安装包,按照步骤安装即可,但要注意版本保证和MATLAB的版本一致。

MATLAB COMPILER

安装MATLAB的时候勾选这类产品即可。

使用时,在命令行输入deploytools即可调出界面,在里面配置好编译的选项。

对于JAVA Library,需要勾选输入的.m文件,设定Java类名,检查方法名。
JAVA_HOME一定要设置对,COMPILER会使用到javac进行编译。
最后打包出来就是一个jar包,在项目中导入使用即可。
Java应用还需要导入javabuiler.jar,这个包在MATLAB或者MCR的安装目录下都会有。

以Linux上的v95版本MCR为例(对应MATLAB 2018b),路径为
/usr/local/MATLAB/MATLAB_Runtime/v95/toolbox/javabuilder/jar/javabuiler.jar

Call MATLAB Function From JAVA

1
2
3
4
5
% usersolve.m
function [outputArg1,outputArg2, outputArg3] = usersolve(arg)
options = optimoptions('fsolve','Display','off');
[outputArg1, outputArg2, outputArg3] = fsolve(@InputSolve, arg, options);
end

打包成usersolve.jar,放到java项目的lib目录下,添加maven引用。

1
2
3
4
5
6
7
<dependency>
<groupId>algo</groupId>
<artifactId>usersolve</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/usersolve.jar</systemPath>
</dependency>

Controller代码。这里我犯了一个错误,我以为MCR会如同MATLAB一样,从Documents/MATLAB目录下加载.m文件。但是其实,MCR只能执行编译过的文件,而且会为这些具体函数建立一个cache,目录为$USER_HOME/.mcrCache9.5/userso2/
这个目录里可以看到,所有的函数都是二进制格式。
所以我虽然接收并存储了用户的函数代码,但实际上执行的还是之前打包进来的InputSolve函数,并不能动态加载函数,这是个失败例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
public class UserSolve {
@Autowired
private UserSolveService userSolveServicej;

@PostMapping("/usersolve")
synchronized public UserSolveResponseBody userSolve(@RequestBody UserSolveBody userSolveBody) throws IOException {
UserSolveResponseBody userSolveResponseBody = new UserSolveResponseBody();
String funcFile = System.getProperty("juser.home") + "/Documents/MATLAB/InputSolve.m";
OutputStream f = new FileOutputStream(funcFile);
f.write(userSolveBody.getFunction().getBytes());
try {
Object[] res = userSolveService.solve(userSolveBody.getSolution());
userSolveResponseBody.setFlag(((MWNumericArray)res[2]).getInt());

List<List<Double>> x = new ArrayList<>();
Object[] xObj = ((MWNumericArray) res[0]).toArray();
for (int i = 0; i < xObj.length; i++) {
x.add(DoubleStream.of((double[]) xObj[i]).boxed().collect(Collectors.toCollection(ArrayList::new)));
}
userSolveResponseBody.setX(x);
List<List<Double>> fval = new ArrayList<>();
Object[] fvalObj = ((MWNumericArray) res[1]).toArray();
for (int i = 0; i < fvalObj.length; i++) {
fval.add(DoubleStream.of((double[]) fvalObj[i]).boxed().collect(Collectors.toCollection(ArrayList::new)));
}
userSolveResponseBody.setFval(fval);
} catch (MWException e) {
e.printStackTrace();
userSolveResponseBody.setErrorInfo(e.getMessage());
}
return userSolveResponseBody;
}
}

Service代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class UserSolveService {
private UserSolve userSolve;

public UserSolveService() throws MWException {
this.userSolve = new UserSolve();
}

public Object[] solve(List<List<Double>> solution) throws MWException {
Object[] objects = new Object[solution.size()];

for (int i = 0; i < solution.size(); i++) {
objects[i] = solution.get(i).toArray();
}
Object[] result = {null, null, null};
Object[] input = {objects};
userSolve.usersolve(result, input);
return result;
}
}

有意思的事情是,给MATLAB函数传参数,统统都是用Object[]。
并且对象数组可以嵌套,而可用的基本类型有double、int、long。
matlab里的矩阵就可以用double[][]表示,虽然先声明Object[]
在逐层向里面加入数据更为灵活。

MATLAB返回的参数,要么会用NumericArray包装,要么就是裸的数据类型。
只是,不同的内置函数的返回类型究竟对应什么,我也没有弄很清楚。

函数画图可以采用webfigure,java这里就对应jsp。

springboot里要启用jsp,添加如下参数。

1
2
3
4
# application.properties

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
@SessionAttributes(value = {"MyFigure"})
public class Weibull {
@Autowired
private WeibullService weibullService;

@PostMapping("/weibull")
public String weibull(@RequestBody WeibullInputBody weibullInputBody, Model model) {
try {
MWNumericArray numericArray = (MWNumericArray) weibullService.evaluate(weibullInputBody.getData());
model.addAttribute("a1", numericArray.getDouble(1));
model.addAttribute("a2", numericArray.getDouble(2));
model.addAttribute("MyFigure", weibullService.plot(numericArray));
} catch (MWException e) {
e.printStackTrace();
}
return "weibull";
}
}

jsp

1
2
<%@ taglib prefix="wf" uri="http://www.mathworks.com/builderja/webfigures.tld" %>
<wf:web-figure name="MyFigure" scope="session"/>

但是,这个jsp里的web-figure会被替换成一段iframe,然后指向了地址$HOST/WebFigures/$name。也就是说,还会有二次请求,那么显然这个地址会404,因为我们没有配置关于这个地址的处理函数。
实际上它的处理函数应该对应MATLAB提供的一个Servlet:
com.mathworks.toolbox.javabuilder.webfigures.WebFiguresServlet

那么springboot里怎么简单地加入一个别人写的servlet呢,
因为我只找到了一个WebServlet注解,然后它只能加在某个类上,
又因为这个类不是我写的,我只能使了个小技巧,继承了这个类。
或者退回到使用mvc的方式,在web.xml里写servlet标签。

1
2
3
@WebServlet(name = "MyServlet",urlPatterns = "/WebFigures/*")
public class Webfigure extends WebFiguresServlet {
}

App处加上扫描Servlet的注解。

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

MATLAB 函数

1
2
3
4
5
function df = getWebFigure()
f=figure('Visible', 'off');
% plot()
df = webfigure(f)
end

JAVA接收webfigure。

1
2
3
((MWJavaObjectRef)result[0]).get()
// 然后把这个对象设置成session的key-value
// key为jsp中对应的figure name