Search code examples
javasqlsqlplus

Why sqlplus command execution from Java doesnt return the output same as direct command in shell?


I am building dynamic sqlplus command and executing the same from Java, It doesnt work as expected. Whereas same command is executed fine when run in command prompt.

The output when executed direct command (Expected):

    BEGIN
*
ERROR at line 1:
ORA-20004: Data is not ready, please check control-M v8 jobs
ORA-06512: at "GLOBAL_OWNER.PKG_COMMON_UTILS", line 282
ORA-06512: at line 2

The output when executed Java class :

SQL*Plus: Release 12.1.0.2.0 Production

Copyright (c) 1982, 2014, Oracle.  All rights reserved.

Use SQL*Plus to execute SQL, PL/SQL and SQL*Plus statements.

Usage 1: sqlplus -H | -V

    -H             Displays the SQL*Plus version and the
                   usage help.
    -V             Displays the SQL*Plus version.

Usage 2: sqlplus [ [<option>] [{logon | /nolog}] [<start>] ]

  <option> is: [-C <version>] [-L] [-M "<options>"] [-NOLOGINTIME] [-R <level>]
               [-S]

    -C <version>   Sets the compatibility of affected commands to the
                   version specified by <version>.  The version has
                   the form "x.y[.z]".  For example, -C 10.2.0
    -L             Attempts to log on just once, instead of
                   reprompting on error.
    -M "<options>" Sets automatic HTML markup of output.  The options
                   have the form:
                   HTML [ON|OFF] [HEAD text] [BODY text] [TABLE text]
                   [ENTMAP {ON|OFF}] [SPOOL {ON|OFF}] [PRE[FORMAT] {ON|OFF}]
    -NOLOGINTIME   Don't display Last Successful Login Time.
    -R <level>     Sets restricted mode to disable SQL*Plus commands
                   that interact with the file system.  The level can
                   be 1, 2 or 3.  The most restrictive is -R 3 which
                   disables all user commands interacting with the
                   file system.
    -S             Sets silent mode which suppresses the display of
                   the SQL*Plus banner, prompts, and echoing of
                   commands.

  <logon> is: {<username>[/<password>][@<connect_identifier>] | / }
              [AS {SYSDBA | SYSOPER | SYSASM | SYSBACKUP | SYSDG | SYSKM}] [EDITION=value]

    Specifies the database account username, password and connect
    identifier for the database connection.  Without a connect
    identifier, SQL*Plus connects to the default database.

    The AS SYSDBA, AS SYSOPER, AS SYSASM, AS SYSBACKUP, AS SYSDG,
    and AS SYSKM options are database administration privileges.

    <connect_identifier> can be in the form of Net Service Name
    or Easy Connect.

      @[<net_service_name> | [//]Host[:Port]/<service_name>]

        <net_service_name> is a simple name for a service that resolves
        to a connect descriptor.

        Example: Connect to database using Net Service Name and the
                 database net service name is ORCL.

           sqlplus myusername/mypassword@ORCL

        Host specifies the host name or IP address of the database
        server computer.

        Port specifies the listening port on the database server.

        <service_name> specifies the service name of the database you
        want to access.

        Example: Connect to database using Easy Connect and the
                 Service name is ORCL.

           sqlplus myusername/mypassword@Host/ORCL

    The /NOLOG option starts SQL*Plus without connecting to a
    database.

    The EDITION specifies the value for Session Edition.


  <start> is: @<URL>|<filename>[.<ext>] [<parameter> ...]

    Runs the specified SQL*Plus script from a web server (URL) or the
    local file system (filename.ext) with specified parameters that
    will be assigned to substitution variables in the script.

When SQL*Plus starts, and after CONNECT commands, the site profile
(e.g. $ORACLE_HOME/sqlplus/admin/glogin.sql) and the user profile
(e.g. login.sql in the working directory) are run.  The files may
contain SQL*Plus commands.

Java Class used to run sqlplus command.

public class RunSqlPlus {

    public static void main(String[] args){
        try {
            String cmd = " sqlplus -s -LOGON <user_name>/<password>@\"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host= host1.com)(Port=1725))(ADDRESS=(PROTOCOL=TCP)(Host= host2.com)(Port=1725))(LOAD_BALANCE = ON)(FAILOVER = ON) (CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME=service.com)))\" @Load.sql";
            Process process;
            process = Runtime.getRuntime()
                    .exec(cmd);

            StreamGobbler streamGobbler =
                    new StreamGobbler(process.getInputStream(), System.out::println);
            Future<?> future = Executors.newSingleThreadExecutor().submit(streamGobbler);

            int exitCode = process.waitFor();
            assert exitCode == 0;

            future.get();
        }
        catch(IOException |InterruptedException | ExecutionException e)
        {
            e.printStackTrace();
        }
    }


    private static class StreamGobbler implements Runnable {
        private InputStream inputStream;
        private Consumer<String> consumer;

        public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
            this.inputStream = inputStream;
            this.consumer = consumer;
        }

        @Override
        public void run() {
            new BufferedReader(new InputStreamReader(inputStream)).lines()
                    .forEach(consumer);
        }
    }
}

Solution

  • Runtime.exec(String) is deprecated in JDK18 as it isn't always good at splitting the commands with spaces / quotes. Break up the command line arguments properly so that there is no ambiguity:

     String[] cmd = new String[] {
         "sqlplus","-s", "-LOGON",
         "<user_name>/<password>@\"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host= host1.com)(Port=1725))(ADDRESS=(PROTOCOL=TCP)(Host= host2.com)(Port=1725))(LOAD_BALANCE = ON)(FAILOVER = ON) (CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME=service.com)))\""
         "@Load.sql"
     };
    

    You have better control over the sub-process if you replace Runtime.exec(cmd) by ProcessBuilder as you can capture the stdout/stderr to files or redirect stderr to stdout to avoid need to read both streams.