SpringFramework核心技术四:Spring表达式使用(SpEL)

简介: SpEL表达式不同场景下的使用SpEL表达式是非常强大的,在不同的场景下有不同的使用方式,下面一起看看它的使用方法一、文字表达支持的文字表达式的类型是字符串,数值(int,real,hex),布尔值和null。

SpEL表达式不同场景下的使用

SpEL表达式是非常强大的,在不同的场景下有不同的使用方式,下面一起看看它的使用方法


一、文字表达

支持的文字表达式的类型是字符串,数值(int,real,hex),布尔值和null。字符串由单引号分隔。要将单引号本身放入字符串中,请使用两个单引号字符。

下面的清单显示了文字的简单用法。通常,它们不会像这样孤立地使用,而是作为更复杂表达式的一部分,例如在逻辑比较运算符的一侧使用文字。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

数字支持使用负号,指数表示法和小数点。默认情况下,实数使用Double.parseDouble()进行分析。

二、Properties, Arrays, Lists, Maps, Indexers

使用属性引用进行浏览很容易:只需使用句点来指示嵌套的属性值。Inventor类,pupin和tesla 的实例中填入了示例中使用的Classes一节中列出的数据。为了“向下”导航并获得特斯拉的出生年份和普平的出生城市,使用了以下表达式。

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

对于属性名称的第一个字母,不区分大小写。数组和列表的内容使用方括号表示法获得。

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        context, tesla, String.class);

// Members List

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        context, ieee, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        context, ieee, String.class);

通过指定括号内的文字键值来获得映射的内容。在这种情况下,因为Officer的Map的键是字符串,所以我们可以指定字符串文字。

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");

三、Inline lists

列表可以用{}符号表达式直接表达。

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}本身意味着一个空的列表。出于性能原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示表达式,而不是在每个评估中创建一个新列表。

四、 Inline Maps

map也可以用{key:value}符号表达式直接表达。

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}本身就意味着一张空白的地图。出于性能原因,如果地图本身由固定文字或其他嵌套常量结构(列表或地图)组成,则将创建常量地图来表示表达式,而不是在每次评估中构建新地图。引用地图键是可选的,上面的例子没有使用带引号的键。

五、Array construction

可以使用熟悉的Java语法构建数组,可以选择提供初始化程序以在构建时填充数组。

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context)

在构建多维数组时,目前不允许提供初始化程序。

六、方法

使用典型的Java编程语法调用方法。你也可以在文字上调用方法。可变参数也被支持。

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean.class);

七、操作符号

1.关系运算符

关系运算符; 相等,不等于,小于,小于或等于,大于,大于或等于支持使用标准的操作符表示法。

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

null按照一条简单的规则进行更大/更小的比较:null在这里被视为无(即不为零)。因此,任何其他值总是大于null(X > null总是true),并且没有其他值X < null总是小于(总是false)。
如果您更喜欢数字比较,请避免基于数字的比较,以便null比较零(例如X > 0或X < 0)。

2.除标准关系运算符外,SpEL还支持instanceof基于正则表达式的matches运算符。

// evaluates to false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//evaluates to false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

请谨慎使用原始类型,因为它们会立即装箱到包装类型,因此1 instanceof T(int)计算结果false时1 instanceof T(Integer) 会true按照预期进行计算。

每个符号运算符也可以被指定为纯粹的字母等价。这避免了所使用的符号对嵌入表达式的文档类型(例如XML文档)具有特殊含义的问题。文本等价物如下所示:lt(<),gt(>),le(<=),ge(>=),eq(==), ne(!=),div(/),mod(%),not(!)。这些不区分大小写。

3.逻辑运算符

The logical operators that are supported are and, or, and not. Their use is demonstrated below.

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

4.Mathematical operators

加法运算符可用于数字和字符串。减法,乘法和除法只能用于数字。其他支持的数学运算符是模数(%)和指数函数(^)。标准运算符优先级被强制执行。以下演示这些操作员。

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class);  // 'test string'

// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000

// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0

// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0

// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21

八、Assignment

属性的设置是通过使用赋值运算符完成的。这通常在呼叫中完成,setValue但也可以在呼叫中完成getValue。

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
        "Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);

九、类型

特殊T运算符可用于指定java.lang.Class(类型)的实例 。也使用此运算符调用静态方法。在 StandardEvaluationContext使用TypeLocator查找类型和 StandardTypeLocator(可替换)是建立与java.lang包的理解。这意味着T()对java.lang中类型的引用不需要完全限定,但所有其他类型引用必须是。

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);

十、构造函数

可以使用new运算符调用构造函数。除了基本类型和String(可以使用int,float等)之外,全限定类名应该用于所有类。

//Inventor需要自己创建
Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

十一、变量

可以使用语法在表达式中引用变量#variableName。变量是使用方法设置setVariable上EvaluationContext实现

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"

1.this和#root变量

该变量#this总是被定义的并且涉及当前评估对象(针对其解析了非限定参考)。变量#root总是被定义并且引用根上下文对象。虽然#this可能会随着表达式的组成部分的不同而变化,但#root始终指向根。

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

十二、功能

您可以通过注册可在表达式字符串内调用的用户定义函数来扩展SpEL。该功能是通过注册EvaluationContext。

Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,给定一个反转字符串的实用方法如下所示:

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++)
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

上述方法可以如下注册和使用:

ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
        StringUtils.class.getDeclaredMethod("reverseString", String.class));

String helloWorldReversed = parser.parseExpression(
        "#reverseString('hello')").getValue(context, String.class);

十三、Bean引用

如果已经使用bean解析器配置了评估上下文,则可以使用@符号从表达式中查找bean

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@foo").getValue(context);

为了访问一个工厂bean本身,bean名称应该以一个&符号为前缀。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

十四、三元运算符(If-Then-Else)

您可以使用三元运算符来执行表达式中的if-then-else条件逻辑。一个最小的例子是:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

在这种情况下,布尔值false将返回字符串值’falseExp’。下面显示了一个更现实的例子。

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

十五、The Elvis Operator

Elvis运算符缩短了三元运算符语法,并用于 Groovy语言。使用三元运算符语法,通常必须重复两次变量,例如:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

Instead you can use the Elvis operator, named for the resemblance to Elvis’ hair style.

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name);  // 'Unknown'

Here is a more complex example.

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

十六、Safe Navigation operator

安全导航运算符用于避免NullPointerException和来自Groovy 语言。通常,当您访问某个对象时,可能需要在访问对象的方法或属性之前验证它是否为空。为了避免这种情况,安全导航运算符将简单地返回null而不是抛出异常。

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

Elvis操作符可用于在表达式中应用默认值,例如在 @Value表达式中:
@Value("#{systemProperties['pop3.port'] ?: 25}")
pop3.port如果它被定义,它将注入一个系统属性,否则注入一个系统属性。

十七、 Collection Selection

选择是一种强大的表达式语言功能,允许您通过从条目中进行选择来将某个源集合转换为另一个源集合。

选择使用语法.?[selectionExpression]。这将过滤集合并返回包含原始元素子集的新集合。例如,选择可以让我们轻松获得塞尔维亚发明者名单:

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

在列表和地图上都可以进行选择。在前一种情况下,针对每个单独列表元素评估选择标准,同时针对映射针对每个映射条目(Java类型的对象)评估选择标准 Map.Entry。地图条目将其键和值作为用于选择的属性进行访问。
这个表达式将返回一个新的地图,该地图由入口值小于27的原始地图的那些元素组成。

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有选定的元素之外,还可以检索第一个或最后一个值。要获得与选择相匹配的第一个条目,语法.^[selectionExpression]同时要获取语法 的最后匹配选择 .$[selectionExpression]。

十八、Collection Projection

投影允许集合驱动子表达式的评估,结果是一个新的集合。投影的语法是.![projectionExpression]。举例来说,最容易理解的是,假设我们有一个发明者名单,但想要他们出生的城市名单。实际上,我们想要为发明人列表中的每个条目评估“placeOfBirth.city”。使用投影:

// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

地图也可以用于驱动投影,在这种情况下,投影表达式将针对地图中的每个条目(表示为Java Map.Entry)进行评估。在地图上投影的结果是一个列表,其中包含对每个地图项的投影表达式的评估。

十九、Expression templating

表达式模板允许将文本文本与一个或多个评估块混合。每个评估块都用可以定义的前缀和后缀字符分隔,常见的选择是#{ }用作分隔符。例如,

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

该字符串通过连接文本文本’random number is ‘和评估#{ }分隔符内表达式的结果来计算,在这种情况下是调用该random()方法的结果。该方法的第二个参数parseExpression() 是类型ParserContext。该ParserContext接口用于影响如何解析表达式以支持表达式模板功能。定义TemplateParserContext如下所示。

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

二十、示例中使用的类

Inventor.java

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}

PlaceOfBirth.java

package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

}

Society.java

package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }

}

好啦,基本上这19种用法覆盖了SpEL表达式的所有。

目录
相关文章
|
3月前
|
XML Java 数据库连接
JAVAEE框架整合技术之spring03-SpringJdbcTemplate模板技术和事务处理
JAVAEE框架整合技术之spring03-SpringJdbcTemplate模板技术和事务处理
67 0
|
26天前
|
安全 Java 数据安全/隐私保护
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
57 1
|
5天前
|
安全 Java API
第5章 Spring Security 的高级认证技术(2024 最新版)(上)
第5章 Spring Security 的高级认证技术(2024 最新版)
29 0
|
6天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
1月前
|
Cloud Native Java Docker
【Spring云原生】Spring官宣,干掉原生JVM,推出 Spring Native!整体提升性能!Native镜像技术在Spring中的应用
【Spring云原生】Spring官宣,干掉原生JVM,推出 Spring Native!整体提升性能!Native镜像技术在Spring中的应用
|
2月前
|
XML 前端开发 Java
深入理解Spring EL表达式的高级功能
深入理解Spring EL表达式的高级功能
78 1
|
2月前
|
XML 前端开发 Java
掌握Spring EL表达式的基础知识
掌握Spring EL表达式的基础知识
38 1
|
3月前
|
XML 存储 Java
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术
54 0
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术
|
3月前
|
XML Java 数据格式
JAVAEE框架整合技术之Spring01-IOC教程
JAVAEE框架整合技术之Spring01-IOC教程
122 0
JAVAEE框架整合技术之Spring01-IOC教程
|
3月前
|
SQL Java 数据库连接
Spring 模板技术JdbcTemplate
Spring 模板技术JdbcTemplate
31 0
Spring 模板技术JdbcTemplate