By far the best solution with the minimum maintenance required and what I finally settled with is as follows:
|- build.rs
|- cargo.toml
|- src
|  |- main.rs
|  |- handler.rs
|  |- handlers.rs  //generated
|  |- handlers
|  |  |- handler1.rs
|  |  |- handler2.rs
handler.rs contains the Handler trait:
trait Handler {
    fn handle(message: str) -> bool;
}
handlers.rs is generated at build time via build.rs:
mod handler1; mod handler2; //...
use crate::handler::*;
use handler1::Handler1; use handler2::Handler2; //...
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
  return vec![Box::new(Handler1), Box::new(Handler2)];
}
cargo.toml must have the build entry supplied:
[package]
build = "build.rs"
edition = "2021"
name = "distributed_handlers"
#...
and finally build.rs looks like this:
use std::{env, fs};
macro_rules! print {
    ($($tokens: tt)*) => {
        println!("cargo:warning={}", format!($($tokens)*))
    }
}
const TEMPLATE: &str = r#"
// Generated bindings to modules
$[mods]
use crate::handler::*;
$[uses]
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
    $[inits]
}
"#;
fn main() {
    let cur = env::current_dir().unwrap();
    let path = String::from(cur.to_string_lossy());
    let mut mods = String::from("");
    let mut uses = String::from("");
    let mut inits: Vec<String> = vec![];
    for entry in fs::read_dir(path.clone() + "/src/handlers").unwrap() {
        let entry: fs::DirEntry = entry.unwrap();
        let name: String = entry.file_name().into_string().unwrap();
        let name: String = String::from(name.split_at(name.len() - 3).0);
        let mut proper: String = name.clone();
        let proper: String = format!("{}{proper}", proper.remove(0).to_uppercase());
        mods.push_str(&String::from(format!("mod {};", name)));
        uses.push_str(&String::from(format!("use {}::{};", name, proper)));
        inits.push(format!("Box::new({})", proper));
    }
    let inits = format!("return vec![{}];", inits.join(","));
    let mut template = String::from(TEMPLATE);
    template = template.replace("$[mods]", &mods);
    template = template.replace("$[uses]", &uses);
    template = template.replace("$[inits]", &inits);
    let _ = fs::write(path.clone() + "/src/handlers.rs", template.clone());
    for s in template.split("\n") {
        print!("{s}");
    }
    print!("Written to {path}/src/handlers.rs")
}
The above code assumes your module is named the same (just different casing) from the struct / handler itself. A full example is on github.
Edit: I refactored the build script, maybe it's a little more readable now:
let current_dir = env::current_dir().unwrap();
let path = current_dir.to_string_lossy().to_string() + "/src/people";
let (mods, uses, inits) = fs::read_dir(path.clone())
    .unwrap()
    .map(|entry| {
        let entry = entry.unwrap();
        let name = entry.file_name().into_string().unwrap();
        let name = String::from(name.split_at(name.len() - 3).0);
        let proper = name.chars().next().unwrap().to_uppercase().to_string() + &name[1..];
        (
            format!("mod {name};\n"),
            format!("use {name}::{proper};\n"),
            format!("Box::new({proper})"),
        )
    })
    .fold(
        (String::new(), String::new(), Vec::new()),
        |(mut mods, mut uses, mut inits), (m, u, i)| {
            mods.push_str(&m);
            uses.push_str(&u);
            inits.push(i);
            (mods, uses, inits)
        },
    );
let inits = inits.join(",");
let template = TEMPLATE
    .replace("$[mods]", &mods)
    .replace("$[uses]", &uses)
    .replace("$[inits]", &inits);
let _ = fs::write(
    current_dir.to_string_lossy().to_string() + "/src/people.rs",
    template.clone(),
);