Java UDAF

更新时间:
复制 MD 格式

This topic describes how to write a user-defined aggregate function (UDAF) in Java.

UDAF code structure

You can write a Java UDAF in IntelliJ IDEA with Maven or MaxCompute Studio. The code must include the following components:

  • Java package: Optional.

    You can package the Java classes that you define to make them easier to find and reuse.

  • Required classes and annotations: Required.

    You must import the com.aliyun.odps.udf.Aggregator class and use the @Resolve annotation (com.aliyun.odps.udf.annotation.Resolve). The com.aliyun.odps.udf.UDFException class is optional and can be used for error handling. If you need to use other UDAF-related classes or complex data types, import the required classes as described in Overview of MaxCompute UDFs.

  • @Resolve annotation: Required.

    The format is @Resolve(<signature>), where signature is the function signature that defines the data types of the input parameters and the return value. The function signature of a UDAF cannot be determined through reflection and can only be obtained by using the @Resolve annotation, such as @Resolve("smallint->varchar(10)"). For more information about the @Resolve annotation, see @Resolve annotation.

  • Custom Java class: Required.

    This class is the organizational unit of the UDAF code. It defines the variables and methods that implement your business logic.

  • Methods for the Java class: Required.

    Your Java class must extend the com.aliyun.odps.udf.Aggregator class and implement the following methods.

    import com.aliyun.odps.udf.ContextFunction;
    import com.aliyun.odps.udf.ExecutionContext;
    import com.aliyun.odps.udf.UDFException;
    public abstract class Aggregator implements ContextFunction {
        // The initialization method.
        @Override
        public void setup(ExecutionContext ctx) throws UDFException {
        }
        // The termination method.
        @Override
        public void close() throws UDFException {
        }
        // Creates an aggregation buffer.
        abstract public Writable newBuffer();
        // The iterate method.
        // buffer is an aggregation buffer that holds intermediate, summarized data. In map tasks, it aggregates data for a group, and this method is executed once for each row.
        // Writable[] represents a row of data, which refers to the input columns in the code. For example, writable[0] refers to the first column, and writable[1] refers to the second column.
        // args are the parameters specified when calling the UDAF in SQL. The args array itself cannot be null, but its elements can be null, which indicates that the corresponding input data is null.
        abstract public void iterate(Writable buffer, Writable[] args) throws UDFException;
        // The terminate method.
        abstract public Writable terminate(Writable buffer) throws UDFException;
        // The merge method.
        abstract public void merge(Writable buffer, Writable partial) throws UDFException;
    }

    The iterate, merge, and terminate methods are the three core methods that implement the main logic of a UDAF. You must also implement a custom writable buffer.

    A writable buffer converts in-memory objects into a byte sequence (or another data transfer protocol) to facilitate disk persistence and network transmission. Because MaxCompute uses distributed computing to process aggregate functions, it must serialize and deserialize data to transfer it between workers.

    When you write a Java UDAF, you can use Java types or Java Writable types. For more information about the mappings between data types supported by MaxCompute and Java data types, see Data types.

The following code provides an example of a UDAF.

// Package the defined Java class in org.alidata.odps.udaf.examples.
package org.alidata.odps.udaf.examples;
// Import the required base classes.
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import com.aliyun.odps.io.DoubleWritable;
import com.aliyun.odps.io.Writable;
import com.aliyun.odps.udf.Aggregator;
import com.aliyun.odps.udf.UDFException;
import com.aliyun.odps.udf.annotation.Resolve;
// Define the custom Java class.
// Specify the @Resolve annotation.
@Resolve("double->double")
public class AggrAvg extends Aggregator {
// Implement the methods for the Java class.
  private static class AvgBuffer implements Writable {
    private double sum = 0;
    private long count = 0;
    @Override
    public void write(DataOutput out) throws IOException {
      out.writeDouble(sum);
      out.writeLong(count);
    }
    @Override
    public void readFields(DataInput in) throws IOException {
      sum = in.readDouble();
      count = in.readLong();
    }
  }
  private DoubleWritable ret = new DoubleWritable();
  @Override
  public Writable newBuffer() {
    return new AvgBuffer();
  }
  @Override
  public void iterate(Writable buffer, Writable[] args) throws UDFException {
    DoubleWritable arg = (DoubleWritable) args[0];
    AvgBuffer buf = (AvgBuffer) buffer;
    if (arg != null) {
      buf.count += 1;
      buf.sum += arg.get();
    }
  }
  @Override
  public Writable terminate(Writable buffer) throws UDFException {
    AvgBuffer buf = (AvgBuffer) buffer;
    if (buf.count == 0) {
      ret.set(0);
    } else {
      ret.set(buf.sum / buf.count);
    }
    return ret;
  }
  @Override
  public void merge(Writable buffer, Writable partial) throws UDFException {
    AvgBuffer buf = (AvgBuffer) buffer;
    AvgBuffer p = (AvgBuffer) partial;
    buf.sum += p.sum;
    buf.count += p.count;
  }
}
Note

In the preceding UDAF code, the buffer in the iterate and merge methods is reusable. It aggregates input rows into the buffer according to your implementation.

Hard limits

Internet access — UDFs cannot access the Internet by default. To enable Internet access, submit a network connection application. After approval, the MaxCompute technical support team will contact you to establish the connection. For details, see Network Connection Request FormNetwork connection process.

VPC access — UDFs cannot access resources in a virtual private cloud (VPC) by default. To enable VPC access, establish a network connection between MaxCompute and the VPC. For details, see Use UDFs to access resources in VPCs.

Unsupported table types — UDFs, UDAFs, and UDTFs cannot read data from the following table types:

  • Tables on which schema evolution has been performed

  • Tables that contain complex data types

  • Tables that contain JSON data types

  • Transactional tables

Usage notes

When you write a Java UDAF, take note of the following points:

  • Including classes with the same name but different logic in the JAR files of different UDAFs can lead to unexpected results or compilation failures. For example, assume UDAF1 and UDAF2 correspond to the resource JAR files udaf1.jar and udaf2.jar, respectively. If both JAR files contain a class named com.aliyun.UserFunction.class but with different implementations, MaxCompute loads one of the classes unpredictably when both UDAF1 and UDAF2 are called in the same SQL statement.

  • In a Java UDAF, input parameters and the return value must be object types, such as String and Long, not primitive types.

  • NULL values in SQL are represented by Java NULL. Java primitive types cannot represent NULL values in SQL and are not allowed.

@Resolve annotation

The format of the @Resolve annotation is as follows.

@Resolve(<signature>)

The signature is a string that identifies the data types of the input parameters and return value. When a UDAF is executed, the types of its input parameters and return value must match the types specified in the function signature. During semantic parsing, the system checks for usages that do not conform to the function signature and reports an error if a type mismatch occurs. The specific format is as follows.

'arg_type_list -> type'

Description:

  • arg_type_list: Represents the data types of the input parameters. Multiple input parameters can be specified, separated by commas (,). Supported data types are BIGINT, STRING, DOUBLE, BOOLEAN, DATETIME, DECIMAL, FLOAT, BINARY, DATE, DECIMAL(precision,scale), CHAR, VARCHAR, complex data types (ARRAY, MAP, STRUCT), and nested complex data types.

    arg_type_list also supports an asterisk (*) or an empty string ('').

    • If arg_type_list is an asterisk (*), it indicates that the function accepts any number of input parameters.

    • If arg_type_list is an empty string (''), it indicates that the function has no input parameters.

    For more information about the extended syntax of the Resolve annotation, see Dynamic parameters for UDAFs and UDTFs.

  • type: Represents the data type of the return value. A UDAF returns only one column. Supported data types include BIGINT, STRING, DOUBLE, BOOLEAN, DATETIME, DECIMAL, FLOAT, BINARY, DATE, DECIMAL(precision,scale), complex data types (ARRAY, MAP, STRUCT), and nested complex data types.

Note

When you write UDAF code, you can select appropriate data types based on the data type edition of your MaxCompute project. For more information about data type editions and the data types supported by each edition, see Data type versions.

The following are examples of valid @Resolve annotations.

@Resolve example

Description

@Resolve('bigint,double->string')

The input parameter types are BIGINT and DOUBLE, and the return value type is STRING.

@Resolve('*->string')

Accepts any number of input parameters, and the return value type is STRING.

@Resolve('->double')

Accepts no input parameters, and the return value type is DOUBLE.

@Resolve('array<bigint>->struct<x:string, y:int>')

The input parameter type is ARRAY<BIGINT>, and the return value type is STRUCT<x:STRING, y:INT>.

Data types

The data types supported by MaxCompute vary based on the data type edition. Starting from MaxCompute 2.0, more data types are added, including complex data types such as ARRAY, MAP, and STRUCT. For more information about MaxCompute data type editions, see Data type editions.

Your Java UDAF must use data types that map to those in MaxCompute. The following table describes these mappings.

MaxCompute type

Java type

Java Writable type

TINYINT

java.lang.Byte

ByteWritable

SMALLINT

java.lang.Short

ShortWritable

INT

java.lang.Integer

IntWritable

BIGINT

java.lang.Long

LongWritable

FLOAT

java.lang.Float

FloatWritable

DOUBLE

java.lang.Double

DoubleWritable

DECIMAL

java.math.BigDecimal

BigDecimalWritable

BOOLEAN

java.lang.Boolean

BooleanWritable

STRING

java.lang.String

Text

VARCHAR

com.aliyun.odps.data.Varchar

VarcharWritable

BINARY

com.aliyun.odps.data.Binary

BytesWritable

DATE

java.sql.Date

DateWritable

DATETIME

java.util.Date

DatetimeWritable

TIMESTAMP

java.sql.Timestamp

TimestampWritable

INTERVAL_YEAR_MONTH

N/A

IntervalYearMonthWritable

INTERVAL_DAY_TIME

N/A

IntervalDayTimeWritable

ARRAY

java.util.List

N/A

MAP

java.util.Map

N/A

STRUCT

com.aliyun.odps.data.Struct

N/A

Note

The input parameters or return value of a UDAF can use the Java Writable Type only if your MaxCompute project uses the MaxCompute V2.0 data type edition.

Usage

After you develop the Java UDAF by following the development process, you can call it in MaxCompute SQL as follows:

  • Use a user-defined function in the current MaxCompute project: The usage is similar to that of a built-in function.

  • Use a user-defined function across projects: To use a user-defined function from Project B in Project A, use a cross-project sharing statement. For example: select B:udf_in_other_project(arg0, arg1) as res from table_t;. For more information about cross-project sharing, see Access resources across projects by using packages.

For a complete example of how to develop and call a Java UDAF by using MaxCompute Studio, see Example.

Example

This example shows how to use MaxCompute Studio to develop a UDAF named AggrAvg that calculates an average value. The following figure illustrates the logic.

求平均值逻辑

  1. Input data slicing: MaxCompute follows the MapReduce processing workflow to split input data into slices that a worker can process efficiently.

    You must configure the slice size by using the odps.stage.mapper.split.size parameter. For more information about the slicing logic, see MapReduce processing workflow.

  2. First phase of average calculation: Each worker counts the number of data records and calculates their sum within its slice. The count and sum from each slice are considered an intermediate result.

  3. Second phase of average calculation: The intermediate results from each slice in the first phase are aggregated.

  4. Final output: r.sum/r.count is the average of all input data.

The following steps describe how to develop and call the Java UDAF:

  1. Prepare the environment.

    Before you can develop and debug a UDF in MaxCompute Studio, you must install MaxCompute Studio and connect it to a MaxCompute project. For more information, see the following topics:

    1. Install MaxCompute Studio

    2. Connect to a MaxCompute project

    3. Create a MaxCompute Java module

  2. Write the UDAF code

    1. In the Project explorer, right-click the source code directory of the module (src > main > java) and select New > MaxCompute Java.新建Java Class

    2. In the Create new MaxCompute java class dialog box, click UDAF, enter a name in the Name field, and press Enter. For this example, name the Java class AggrAvg.

      The Name is the name of the MaxCompute Java class. If you have not created a package, enter the name in the packagename.classname format. A package is automatically generated.

    3. In the code editor, paste the following UDAF code.

      import java.io.DataInput;
      import java.io.DataOutput;
      import java.io.IOException;
      import com.aliyun.odps.io.DoubleWritable;
      import com.aliyun.odps.io.Writable;
      import com.aliyun.odps.udf.Aggregator;
      import com.aliyun.odps.udf.UDFException;
      import com.aliyun.odps.udf.annotation.Resolve;
      @Resolve("double->double")
      public class AggrAvg extends Aggregator {
        private static class AvgBuffer implements Writable {
          private double sum = 0;
          private long count = 0;
          @Override
          public void write(DataOutput out) throws IOException {
            out.writeDouble(sum);
            out.writeLong(count);
          }
          @Override
          public void readFields(DataInput in) throws IOException {
            sum = in.readDouble();
            count = in.readLong();
          }
        }
        private DoubleWritable ret = new DoubleWritable();
        @Override
        public Writable newBuffer() {
          return new AvgBuffer();
        }
        @Override
        public void iterate(Writable buffer, Writable[] args) throws UDFException {
          DoubleWritable arg = (DoubleWritable) args[0];
          AvgBuffer buf = (AvgBuffer) buffer;
          if (arg != null) {
            buf.count += 1;
            buf.sum += arg.get();
          }
        }
        @Override
        public Writable terminate(Writable buffer) throws UDFException {
          AvgBuffer buf = (AvgBuffer) buffer;
          if (buf.count == 0) {
            ret.set(0);
          } else {
            ret.set(buf.sum / buf.count);
          }
          return ret;
        }
        @Override
        public void merge(Writable buffer, Writable partial) throws UDFException {
          AvgBuffer buf = (AvgBuffer) buffer;
          AvgBuffer p = (AvgBuffer) partial;
          buf.sum += p.sum;
          buf.count += p.count;
        }
      }
  3. Debug the UDAF locally

    For more debugging operations, see Debug UDFs by running them locally.

    Right-click the AggrAvg.java file in your project and choose Run 'AggrAvg.main()'. In the Run/Debug Configurations dialog box that appears, configure the following parameters: MaxCompute project to local, MaxCompute table to kmeans_in, Table columns to dim1,dim2, Download Record limit to 100, and Data Column Separator to a comma. Then, click OK.

    Note

    You can use the data shown in the figure as a reference for the run parameters.

  4. Package the UDAF that you created into a JAR file, upload the JAR file to a MaxCompute project, and register the function. For example, the function is named user_udaf.

    For more information about packaging operations, see Steps.

    In IntelliJ IDEA, right-click the Java file that contains the UDAF and choose Deploy to server.... In the Package a jar, submit resource and register function dialog box, configure MaxCompute project, Resource name, Main class (enter the UDAF class name), and Function name. Select the Force update if already exists checkbox and click OK to complete the deployment.

  5. In the left navigation bar of MaxCompute Studio, click Project Explorer, right-click the target MaxCompute project, start the MaxCompute client, and execute an SQL command to call the newly created UDAF.

    Assume that the target table my_table has the following schema and data.

    +------------+------------+
    | col0       | col1       |
    +------------+------------+
    | 1.2        | 2.0        |
    | 1.6        | 2.1        |
    +------------+------------+

    Run the following SQL statement to call the UDAF.

    select user_udaf(col0) as c0 from my_table;

    The following result is returned.

    +----+
    | c0 |
    +----+
    | 1.4|
    +----+